New: Project Aphrodite

This commit is contained in:
Qstick
2018-11-23 02:04:42 -05:00
parent 65efa15551
commit 8430cb40ab
1080 changed files with 73015 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import _ from 'lodash';
export default function getIndexOfFirstCharacter(items, character) {
return _.findIndex(items, (item) => {
const firstCharacter = item.sortTitle.charAt(0);
if (character === '#') {
return !isNaN(firstCharacter);
}
return firstCharacter === character;
});
}

View File

@@ -0,0 +1,5 @@
function sortByName(a, b) {
return a.name.localeCompare(b.name);
}
export default sortByName;

View File

@@ -0,0 +1,10 @@
import _ from 'lodash';
import isSameCommand from './isSameCommand';
function findCommand(commands, options) {
return _.findLast(commands, (command) => {
return isSameCommand(command.body, options);
});
}
export default findCommand;

View File

@@ -0,0 +1,5 @@
export { default as findCommand } from './findCommand';
export { default as isCommandComplete } from './isCommandComplete';
export { default as isCommandExecuting } from './isCommandExecuting';
export { default as isCommandFailed } from './isCommandFailed';
export { default as isSameCommand } from './isSameCommand';

View File

@@ -0,0 +1,9 @@
function isCommandComplete(command) {
if (!command) {
return false;
}
return command.status === 'complete';
}
export default isCommandComplete;

View File

@@ -0,0 +1,9 @@
function isCommandExecuting(command) {
if (!command) {
return false;
}
return command.status === 'queued' || command.status === 'started';
}
export default isCommandExecuting;

View File

@@ -0,0 +1,12 @@
function isCommandFailed(command) {
if (!command) {
return false;
}
return command.status === 'failed' ||
command.status === 'aborted' ||
command.status === 'cancelled' ||
command.status === 'orphaned';
}
export default isCommandFailed;

View File

@@ -0,0 +1,24 @@
import _ from 'lodash';
function isSameCommand(commandA, commandB) {
if (commandA.name.toLocaleLowerCase() !== commandB.name.toLocaleLowerCase()) {
return false;
}
for (const key in commandB) {
if (key !== 'name') {
const value = commandB[key];
if (Array.isArray(value)) {
if (_.difference(value, commandA[key]).length > 0) {
return false;
}
} else if (value !== commandA[key]) {
return false;
}
}
}
return true;
}
export default isSameCommand;

View File

@@ -0,0 +1,7 @@
export const TAB = 9;
export const ENTER = 13;
export const SHIFT = 16;
export const CONTROL = 17;
export const ESCAPE = 27;
export const UP_ARROW = 38;
export const DOWN_ARROW = 40;

View File

@@ -0,0 +1,33 @@
import moment from 'moment';
import isAfter from 'Utilities/Date/isAfter';
import isBefore from 'Utilities/Date/isBefore';
import * as filterTypes from 'Helpers/Props/filterTypes';
export default function(itemValue, filterValue, type) {
if (!itemValue) {
return false;
}
switch (type) {
case filterTypes.LESS_THAN:
return moment(itemValue).isBefore(filterValue);
case filterTypes.GREATER_THAN:
return moment(itemValue).isAfter(filterValue);
case filterTypes.IN_LAST:
return (
isAfter(itemValue, { [filterValue.time]: filterValue.value * -1 }) &&
isBefore(itemValue)
);
case filterTypes.IN_NEXT:
return (
isAfter(itemValue) &&
isBefore(itemValue, { [filterValue.time]: filterValue.value })
);
default:
return false;
}
}

View File

@@ -0,0 +1,11 @@
import moment from 'moment';
function formatDate(date, dateFormat) {
if (!date) {
return '';
}
return moment(date).format(dateFormat);
}
export default formatDate;

View File

@@ -0,0 +1,39 @@
import moment from 'moment';
import formatTime from './formatTime';
import isToday from './isToday';
import isTomorrow from './isTomorrow';
import isYesterday from './isYesterday';
function getRelativeDay(date, includeRelativeDate) {
if (!includeRelativeDate) {
return '';
}
if (isYesterday(date)) {
return 'Yesterday, ';
}
if (isToday(date)) {
return 'Today, ';
}
if (isTomorrow(date)) {
return 'Tomorrow, ';
}
return '';
}
function formatDateTime(date, dateFormat, timeFormat, { includeSeconds = false, includeRelativeDay = false } = {}) {
if (!date) {
return '';
}
const relativeDay = getRelativeDay(date, includeRelativeDay);
const formattedDate = moment(date).format(dateFormat);
const formattedTime = formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds });
return `${relativeDay}${formattedDate} ${formattedTime}`;
}
export default formatDateTime;

View File

@@ -0,0 +1,19 @@
import moment from 'moment';
function formatTime(date, timeFormat, { includeMinuteZero = false, includeSeconds = false } = {}) {
if (!date) {
return '';
}
if (includeSeconds) {
timeFormat = timeFormat.replace(/\(?:mm\)?/, ':mm:ss');
} else if (includeMinuteZero) {
timeFormat = timeFormat.replace('(:mm)', ':mm');
} else {
timeFormat = timeFormat.replace('(:mm)', '');
}
return moment(date).format(timeFormat);
}
export default formatTime;

View File

@@ -0,0 +1,24 @@
import moment from 'moment';
import padNumber from 'Utilities/Number/padNumber';
function formatTimeSpan(timeSpan) {
if (!timeSpan) {
return '';
}
const duration = moment.duration(timeSpan);
const days = duration.get('days');
const hours = padNumber(duration.get('hours'), 2);
const minutes = padNumber(duration.get('minutes'), 2);
const seconds = padNumber(duration.get('seconds'), 2);
const time = `${hours}:${minutes}:${seconds}`;
if (days > 0) {
return `${days}d ${time}`;
}
return time;
}
export default formatTimeSpan;

View File

@@ -0,0 +1,42 @@
import moment from 'moment';
import formatTime from 'Utilities/Date/formatTime';
import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday';
import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday';
function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
if (!date) {
return null;
}
const isTodayDate = isToday(date);
if (isTodayDate && timeForToday && timeFormat) {
return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds });
}
if (!showRelativeDates) {
return moment(date).format(shortDateFormat);
}
if (isYesterday(date)) {
return 'Yesterday';
}
if (isTodayDate) {
return 'Today';
}
if (isTomorrow(date)) {
return 'Tomorrow';
}
if (isInNextWeek(date)) {
return moment(date).format('dddd');
}
return moment(date).format(shortDateFormat);
}
export default getRelativeDate;

View File

@@ -0,0 +1,17 @@
import moment from 'moment';
function isAfter(date, offsets = {}) {
if (!date) {
return false;
}
const offsetTime = moment();
Object.keys(offsets).forEach((key) => {
offsetTime.add(offsets[key], key);
});
return moment(date).isAfter(offsetTime);
}
export default isAfter;

View File

@@ -0,0 +1,17 @@
import moment from 'moment';
function isBefore(date, offsets = {}) {
if (!date) {
return false;
}
const offsetTime = moment();
Object.keys(offsets).forEach((key) => {
offsetTime.add(offsets[key], key);
});
return moment(date).isBefore(offsetTime);
}
export default isBefore;

View File

@@ -0,0 +1,11 @@
import moment from 'moment';
function isInNextWeek(date) {
if (!date) {
return false;
}
const now = moment();
return moment(date).isBetween(now, now.clone().add(6, 'days').endOf('day'));
}
export default isInNextWeek;

View File

@@ -0,0 +1,11 @@
import moment from 'moment';
function isSameWeek(date) {
if (!date) {
return false;
}
return moment(date).isSame(moment(), 'week');
}
export default isSameWeek;

View File

@@ -0,0 +1,11 @@
import moment from 'moment';
function isToday(date) {
if (!date) {
return false;
}
return moment(date).isSame(moment(), 'day');
}
export default isToday;

View File

@@ -0,0 +1,11 @@
import moment from 'moment';
function isTomorrow(date) {
if (!date) {
return false;
}
return moment(date).isSame(moment().add(1, 'day'), 'day');
}
export default isTomorrow;

View File

@@ -0,0 +1,11 @@
import moment from 'moment';
function isYesterday(date) {
if (!date) {
return false;
}
return moment(date).isSame(moment().subtract(1, 'day'), 'day');
}
export default isYesterday;

View File

@@ -0,0 +1,21 @@
import _ from 'lodash';
import { update } from 'Store/Actions/baseActions';
function updateEpisodes(section, episodes, episodeIds, options) {
const data = _.reduce(episodes, (result, item) => {
if (episodeIds.indexOf(item.id) > -1) {
result.push({
...item,
...options
});
} else {
result.push(item);
}
return result;
}, []);
return update({ section, data });
}
export default updateEpisodes;

View File

@@ -0,0 +1,19 @@
export default function findSelectedFilters(selectedFilterKey, filters = [], customFilters = []) {
if (!selectedFilterKey) {
return [];
}
let selectedFilter = filters.find((f) => f.key === selectedFilterKey);
if (!selectedFilter) {
selectedFilter = customFilters.find((f) => f.id === selectedFilterKey);
}
if (!selectedFilter) {
// TODO: throw in dev
console.error('Matching filter not found');
return [];
}
return selectedFilter.filters;
}

View File

@@ -0,0 +1,11 @@
export default function getFilterValue(filters, filterKey, filterValueKey, defaultValue) {
const filter = filters.find((f) => f.key === filterKey);
if (!filter) {
return defaultValue;
}
const filterValue = filter.filters.find((f) => f.key === filterValueKey);
return filterValue ? filterValue.value : defaultValue;
}

View File

@@ -0,0 +1,18 @@
function getNewMovie(movie, payload) {
const {
rootFolderPath,
monitor,
qualityProfileId,
tags
} = payload;
movie.monitored = monitor === 'true';
movie.qualityProfileId = qualityProfileId;
movie.rootFolderPath = rootFolderPath;
movie.tags = tags;
return movie;
}
export default getNewMovie;

View File

@@ -0,0 +1,23 @@
import { kinds } from 'Helpers/Props';
function getProgressBarKind(status, monitored, hasFile) {
if (status === 'announced') {
return kinds.PRIMARY;
}
if (hasFile && monitored) {
return kinds.SUCCESS;
}
if (hasFile && !monitored) {
return kinds.DEFAULT;
}
if (monitored) {
return kinds.DANGER;
}
return kinds.WARNING;
}
export default getProgressBarKind;

View File

@@ -0,0 +1,16 @@
function convertToBytes(input, power, binaryPrefix) {
const size = Number(input);
if (isNaN(size)) {
return '';
}
const prefix = binaryPrefix ? 1024 : 1000;
const multiplier = Math.pow(prefix, power);
const result = size * multiplier;
return Math.round(result);
}
export default convertToBytes;

View File

@@ -0,0 +1,17 @@
function formatAge(age, ageHours, ageMinutes) {
age = Math.round(age);
ageHours = parseFloat(ageHours);
ageMinutes = ageMinutes && parseFloat(ageMinutes);
if (age < 2 && ageHours) {
if (ageHours < 2 && !!ageMinutes) {
return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? 'minute' : 'minutes'}`;
}
return `${ageHours.toFixed(1)} ${ageHours === 1 ? 'hour' : 'hours'}`;
}
return `${age} ${age === 1 ? 'day' : 'days'}`;
}
export default formatAge;

View File

@@ -0,0 +1,16 @@
import filesize from 'filesize';
function formatBytes(input) {
const size = Number(input);
if (isNaN(size)) {
return '';
}
return filesize(size, {
base: 2,
round: 1
});
}
export default formatBytes;

View File

@@ -0,0 +1,10 @@
function padNumber(input, width, paddingCharacter = 0) {
if (input == null) {
return '';
}
input = `${input}`;
return input.length >= width ? input : new Array(width - input.length + 1).join(paddingCharacter) + input;
}
export default padNumber;

View File

@@ -0,0 +1,5 @@
export default function roundNumber(input, decimalPlaces = 1) {
const multiplier = Math.pow(10, decimalPlaces);
return Math.round(input * multiplier) / multiplier;
}

View File

@@ -0,0 +1,11 @@
function getErrorMessage(xhr, fallbackErrorMessage) {
if (!xhr || !xhr.responseJSON || !xhr.responseJSON.message) {
return fallbackErrorMessage;
}
const message = xhr.responseJSON.message;
return message || fallbackErrorMessage;
}
export default getErrorMessage;

View File

@@ -0,0 +1,10 @@
import _ from 'lodash';
function hasDifferentItems(prevItems, currentItems, idProp = 'id') {
const diff1 = _.differenceBy(prevItems, currentItems, (item) => item[idProp]);
const diff2 = _.differenceBy(currentItems, prevItems, (item) => item[idProp]);
return diff1.length > 0 || diff2.length > 0;
}
export default hasDifferentItems;

View File

@@ -0,0 +1,15 @@
import _ from 'lodash';
function selectUniqueIds(items, idProp) {
const ids = _.reduce(items, (result, item) => {
if (item[idProp]) {
result.push(item[idProp]);
}
return result;
}, []);
return _.uniq(ids);
}
export default selectUniqueIds;

View File

@@ -0,0 +1,16 @@
export default function getQualities(qualities) {
if (!qualities) {
return [];
}
return qualities.reduce((acc, item) => {
if (item.quality) {
acc.push(item.quality);
} else {
const groupQualities = item.items.map((i) => i.quality);
acc.push(...groupQualities);
}
return acc;
}, []);
}

View File

@@ -0,0 +1,26 @@
import $ from 'jquery';
module.exports = {
resolutions: {
desktopLarge: 1200,
desktop: 992,
tablet: 768,
mobile: 480
},
isDesktopLarge() {
return $(window).width() < this.resolutions.desktopLarge;
},
isDesktop() {
return $(window).width() < this.resolutions.desktop;
},
isTablet() {
return $(window).width() < this.resolutions.tablet;
},
isMobile() {
return $(window).width() < this.resolutions.mobile;
}
};

View File

@@ -0,0 +1,42 @@
import _ from 'lodash';
import getSectionState from 'Utilities/State/getSectionState';
function getProviderState(payload, getState, section) {
const {
id,
...otherPayload
} = payload;
const state = getSectionState(getState(), section, true);
const pendingChanges = Object.assign({}, state.pendingChanges, otherPayload);
const pendingFields = state.pendingChanges.fields || {};
delete pendingChanges.fields;
const item = id ? _.find(state.items, { id }) : state.selectedSchema || state.schema || {};
if (item.fields) {
pendingChanges.fields = _.reduce(item.fields, (result, field) => {
const name = field.name;
const value = pendingFields.hasOwnProperty(name) ?
pendingFields[name] :
field.value;
// Only send the name and value to the server
result.push({
name,
value
});
return result;
}, []);
}
const result = Object.assign({}, item, pendingChanges);
delete result.presets;
return result;
}
export default getProviderState;

View File

@@ -0,0 +1,22 @@
import _ from 'lodash';
function getSectionState(state, section, isFullStateTree = false) {
if (isFullStateTree) {
return _.get(state, section);
}
const [, subSection] = section.split('.');
if (subSection) {
return Object.assign({}, state[subSection]);
}
// TODO: Remove in favour of using subSection
if (state.hasOwnProperty(section)) {
return Object.assign({}, state[section]);
}
return Object.assign({}, state);
}
export default getSectionState;

View File

@@ -0,0 +1,34 @@
import _ from 'lodash';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
function applySchemaDefaults(selectedSchema, schemaDefaults) {
if (!schemaDefaults) {
return selectedSchema;
} else if (_.isFunction(schemaDefaults)) {
return schemaDefaults(selectedSchema);
}
return Object.assign(selectedSchema, schemaDefaults);
}
function selectProviderSchema(state, section, payload, schemaDefaults) {
const newState = getSectionState(state, section);
const {
implementation,
presetName
} = payload;
const selectedImplementation = _.find(newState.schema, { implementation });
const selectedSchema = presetName ?
_.find(selectedImplementation.presets, { name: presetName }) :
selectedImplementation;
newState.selectedSchema = applySchemaDefaults(_.cloneDeep(selectedSchema), schemaDefaults);
return updateSectionState(state, section, newState);
}
export default selectProviderSchema;

View File

@@ -0,0 +1,16 @@
function updateSectionState(state, section, newState) {
const [, subSection] = section.split('.');
if (subSection) {
return Object.assign({}, state, { [subSection]: newState });
}
// TODO: Remove in favour of using subSection
if (state.hasOwnProperty(section)) {
return Object.assign({}, state, { [section]: newState });
}
return Object.assign({}, state, newState);
}
export default updateSectionState;

View File

@@ -0,0 +1,5 @@
export default function combinePath(isWindows, basePath, paths = []) {
const slash = isWindows ? '\\' : '/';
return `${basePath}${slash}${paths.join(slash)}`;
}

View File

@@ -0,0 +1,6 @@
export default function generateUUIDv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
// eslint-disable-next-line no-bitwise
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}

View File

@@ -0,0 +1,3 @@
export default function isString(possibleString) {
return typeof possibleString === 'string' || possibleString instanceof String;
}

View File

@@ -0,0 +1,36 @@
import _ from 'lodash';
import qs from 'qs';
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils
const anchor = document.createElement('a');
export default function parseUrl(url) {
anchor.href = url;
// The `origin`, `password`, and `username` properties are unavailable in
// Opera Presto. We synthesize `origin` if it's not present. While `password`
// and `username` are ignored intentionally.
const properties = _.pick(
anchor,
'hash',
'host',
'hostname',
'href',
'origin',
'pathname',
'port',
'protocol',
'search'
);
properties.isAbsolute = (/^[\w:]*\/\//).test(url);
if (properties.search) {
// Remove leading ? from querystring before parsing.
properties.params = qs.parse(properties.search.substring(1));
} else {
properties.params = {};
}
return properties;
}

View File

@@ -0,0 +1,17 @@
import _ from 'lodash';
function split(input, separator = ',') {
if (!input) {
return [];
}
return _.reduce(input.split(separator), (result, s) => {
if (s) {
result.push(s);
}
return result;
}, []);
}
export default split;

View File

@@ -0,0 +1,11 @@
function titleCase(input) {
if (!input) {
return '';
}
return input.replace(/\b\w+/g, (match) => {
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
});
}
export default titleCase;

View File

@@ -0,0 +1,17 @@
export default function areAllSelected(selectedState) {
let allSelected = true;
let allUnselected = true;
Object.keys(selectedState).forEach((key) => {
if (selectedState[key]) {
allUnselected = false;
} else {
allSelected = false;
}
});
return {
allSelected,
allUnselected
};
}

View File

@@ -0,0 +1,15 @@
import _ from 'lodash';
function getSelectedIds(selectedState, { parseIds = true } = {}) {
return _.reduce(selectedState, (result, value, id) => {
if (value) {
const parsedId = parseIds ? parseInt(id) : id;
result.push(parsedId);
}
return result;
}, []);
}
export default getSelectedIds;

View File

@@ -0,0 +1,23 @@
import _ from 'lodash';
function getToggledRange(items, id, lastToggled) {
const lastToggledIndex = _.findIndex(items, { id: lastToggled });
const changedIndex = _.findIndex(items, { id });
let lower = 0;
let upper = 0;
if (lastToggledIndex > changedIndex) {
lower = changedIndex;
upper = lastToggledIndex + 1;
} else {
lower = lastToggledIndex;
upper = changedIndex;
}
return {
lower,
upper
};
}
export default getToggledRange;

View File

@@ -0,0 +1,16 @@
import areAllSelected from './areAllSelected';
export default function removeOldSelectedState(state, prevItems) {
const selectedState = {
...state.selectedState
};
prevItems.forEach((item) => {
delete selectedState[item.id];
});
return {
...areAllSelected(selectedState),
selectedState
};
}

View File

@@ -0,0 +1,17 @@
import _ from 'lodash';
function selectAll(selectedState, selected) {
const newSelectedState = _.reduce(Object.keys(selectedState), (result, item) => {
result[item] = selected;
return result;
}, {});
return {
allSelected: selected,
allUnselected: !selected,
lastToggled: null,
selectedState: newSelectedState
};
}
export default selectAll;

View File

@@ -0,0 +1,30 @@
import areAllSelected from './areAllSelected';
import getToggledRange from './getToggledRange';
function toggleSelected(state, items, id, selected, shiftKey) {
const lastToggled = state.lastToggled;
const selectedState = {
...state.selectedState,
[id]: selected
};
if (selected == null) {
delete selectedState[id];
}
if (shiftKey && lastToggled) {
const { lower, upper } = getToggledRange(items, id, lastToggled);
for (let i = lower; i < upper; i++) {
selectedState[items[i].id] = selected;
}
}
return {
...areAllSelected(selectedState),
lastToggled: id,
selectedState
};
}
export default toggleSelected;

View File

@@ -0,0 +1,30 @@
import $ from 'jquery';
export default function createAjaxRequest(ajaxOptions) {
const requestXHR = new window.XMLHttpRequest();
let aborted = false;
let complete = false;
function abortRequest() {
if (!complete) {
aborted = true;
requestXHR.abort();
}
}
const request = $.ajax({
xhr: () => requestXHR,
...ajaxOptions
}).then(null, (xhr, textStatus, errorThrown) => {
xhr.aborted = aborted;
return $.Deferred().reject(xhr, textStatus, errorThrown).promise();
}).always(() => {
complete = true;
});
return {
request,
abortRequest
};
}

View File

@@ -0,0 +1,3 @@
export default function getPathWithUrlBase(path) {
return `${window.Radarr.urlBase}${path}`;
}

View File

@@ -0,0 +1,7 @@
let i = 0;
// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022)
export default function getUniqueElementId() {
return `id-${i++}`;
}

View File

@@ -0,0 +1,7 @@
import MobileDetect from 'mobile-detect';
export default function isMobile() {
const mobileDetect = new MobileDetect(window.navigator.userAgent);
return mobileDetect.mobile() != null;
}

View File

@@ -0,0 +1,28 @@
let currentPopulator = null;
let currentReasons = [];
export function registerPagePopulator(populator, reasons = []) {
currentPopulator = populator;
currentReasons = reasons;
}
export function unregisterPagePopulator(populator) {
if (currentPopulator === populator) {
currentPopulator = null;
currentReasons = [];
}
}
export function repopulatePage(reason) {
if (!currentPopulator) {
return;
}
if (!reason) {
currentPopulator();
}
if (reason && currentReasons.includes(reason)) {
currentPopulator();
}
}

View File

@@ -0,0 +1,9 @@
const pages = {
FIRST: 'first',
PREVIOUS: 'previous',
NEXT: 'next',
LAST: 'last',
EXACT: 'exact'
};
export default pages;

View File

@@ -0,0 +1,40 @@
import $ from 'jquery';
import _ from 'lodash';
function flattenProviderData(providerData) {
return _.reduce(Object.keys(providerData), (result, key) => {
const property = providerData[key];
if (key === 'fields') {
result[key] = property;
} else {
result[key] = property.value;
}
return result;
}, {});
}
function requestAction(payload) {
const {
provider,
action,
providerData,
queryParams
} = payload;
const ajaxOptions = {
url: `/${provider}/action/${action}`,
contentType: 'application/json',
method: 'POST',
data: JSON.stringify(flattenProviderData(providerData))
};
if (queryParams) {
ajaxOptions.url += `?${$.param(queryParams, true)}`;
}
return $.ajax(ajaxOptions);
}
export default requestAction;

View File

@@ -0,0 +1,6 @@
const sectionTypes = {
COLLECTION: 'collection',
MODEL: 'model'
};
export default sectionTypes;

View File

@@ -0,0 +1,12 @@
const serverSideCollectionHandlers = {
FETCH: 'fetch',
FIRST_PAGE: 'firstPage',
PREVIOUS_PAGE: 'previousPage',
NEXT_PAGE: 'nextPage',
LAST_PAGE: 'lastPage',
EXACT_PAGE: 'exactPage',
SORT: 'sort',
FILTER: 'filter'
};
export default serverSideCollectionHandlers;