Skip to content

Instantly share code, notes, and snippets.

@az67128
Last active August 25, 2020 13:22
Show Gist options
  • Save az67128/351a4baa248f7e3a231d29038284afe2 to your computer and use it in GitHub Desktop.
Save az67128/351a4baa248f7e3a231d29038284afe2 to your computer and use it in GitHub Desktop.
API
import {getStore} from '../tools/connect';
import {flattenError} from '../tools/index';
import {environment} from '../configs';
import keysToCamel from './keysToCamel';
import mock from './mock';
import getApiPath from './getApiPath';
export {getApiPath};
const callApi = (path, options) => {
const url = getApiPath(path);
const responseHeaders = {};
if (environment === 'test') return mock(url, options);
return fetch(url, {
credentials: 'include',
headers: options.headers ? options.headers : {'Content-type': 'application/json; charset=utf-8'},
...options,
})
.then((res) => {
if (res.status === 401) {
// timeout to safely complete current store request
const store = getStore();
if (store) setTimeout(() => store.auth.logout(false), 0);
throw new Error('Сессия истекла. Авторизуйтесь заново');
}
if (res.status === 500 || res.status === 504 || res.status === 502) {
throw new Error('На сервере произошла ошибка, повторите попытку позже');
}
return res;
})
.then((res) => {
if (options.withHeaders) {
for (const [name, value] of res.headers.entries()) {
responseHeaders[name] = value;
}
}
switch (options.format) {
case 'json':
return res.json();
case 'text':
return res.text();
case 'blob':
return res.blob();
case 'formData':
return res.formData();
case 'arrayBuffer':
return res.arrayBuffer();
default:
return res.json();
}
})
.then((data) => {
if (data.errorText && data.errorFields) {
const errorText = flattenError(data.errorFields);
const error = new Error(errorText);
error.body = {...data, errorText};
throw error;
} else if (data.errorText) {
const error = new Error(typeof data.errorText === 'string' ? data.errorText : 'Неизвестная Ошибка');
error.body = data;
throw error;
} else {
const responseData = options.toCamel === false ? data : keysToCamel(data);
return options.withHeaders ? {data: responseData, headers: responseHeaders} : responseData;
}
});
};
const api = {
get: (path, options = {}) => callApi(path, {...options, method: 'GET'}),
post: (path, options = {}) => callApi(path, {...options, method: 'POST', body: JSON.stringify(options.body || '')}),
patch: (path, options = {}) => callApi(path, {...options, method: 'PATCH', body: JSON.stringify(options.body || '')}),
put: (path, options = {}) => callApi(path, {...options, method: 'PUT', body: JSON.stringify(options.body || '')}),
delete: (path, options = {}) => callApi(path, {...options, method: 'DELETE', body: JSON.stringify(options.body || '')}),
upload: (path, options = {}) => callApi(path, {...options, method: 'POST', body: options.body, headers: {...(options.headers || {})}}),
};
export default api;
import {types, flow, getRoot} from 'mobx-state-tree';
import api from 'api';
import List from 'stores/commonModels/list';
import Client from './client.js';
const Clients = types
.model('Clients', {
list: types.optional(List(Client), {
searchField: 'name_or_inn_or_phone',
isLoading: true,
}),
})
.actions((self) => {
const model = self;
const {addNotification} = getRoot(self).common;
const getList = flow(function* ajax() {
model.list.isLoading = true;
try {
const {data} = yield api
.get(`api/partner_access/?&sort_type=desc&sort_key=status&${model.list.queryString}`);
model.list.items = data.requests;
model.list.count = data.count;
} catch (err) {
addNotification(err);
}
model.list.isLoading = false;
});
return {getList};
});
export default Clients;
import {types, getRoot, getParent, addMiddleware, createActionTrackingMiddleware2} from 'mobx-state-tree';
import {reaction} from 'mobx';
import history from 'tools/history';
import Filter from './filter';
import DateFilter from './dateFilter';
import SortItem from './sortItem';
/**
*
* List depends on getList action of parent. getList will be triggered on search, filters and pagination.
*
* On component mount call attach action.
* On component unmount call detach action
*
*/
const List = (ItemsModel) => {
const Model = types
.model('List', {
items: types.array(ItemsModel),
count: types.optional(types.number, 0),
offset: types.optional(types.number, 0),
limit: types.optional(types.number, 10),
isLoading: types.optional(types.boolean, false),
filters: types.array(
types.union({
dispatcher(snapshot) {
return snapshot.type === 'date' ? DateFilter : Filter;
},
Filter,
DateFilter,
}),
),
sorts: types.array(SortItem),
searchField: types.maybeNull(types.string),
queryTimeout: types.optional(types.number, 300),
})
.volatile(() => ({
queryTimeoutId: null,
getListActionId: null,
reactionDisposers: [],
historyDisposer: () => {},
}))
.actions((self) => {
const model = self;
const {quickSearch} = getRoot(model);
const {getList} = getParent(model);
const abortGetListMiddleware = (call, next, abort) => {
if (
call.parentActionEvent
&& call.parentActionEvent.id !== model.getListActionId
&& call.parentActionEvent.name === 'getList'
) {
abort(call);
} else {
next(call);
}
};
const getListTrackingMiddleware = createActionTrackingMiddleware2({
filter: (call) => call.name === 'getList',
onStart: (call) => {
model.getListActionId = call.id;
},
onFinish: () => {},
});
addMiddleware(model.items, abortGetListMiddleware);
addMiddleware(getParent(model), getListTrackingMiddleware);
const attach = () => {
model.getStateFromSearch();
model.historyDisposer = history.listen((location, action) => {
if (action === 'PUSH' || action === 'POP') {
setTimeout(() => {
model.getStateFromSearch();
// execute if history not disposed
if (model.historyDisposer) model.scheduleRefresh(0);
}, 0);
}
});
model.reactionDisposers = [
reaction(
() => [
model.filterQueryString,
model.searchQueryString,
model.pagingQueryString,
model.sortQueryString,
],
() => {
const historySearch = new URLSearchParams(history.location.search);
const listSearch = new URLSearchParams(model.queryString);
[
'offset',
'limit',
'sort_by',
...(model.searchField ? [model.searchField] : []),
...model.filters
.map((filter) => {
if (filter.type === 'date') return [`${filter.prefix}from_dt`, `${filter.prefix}to_dt`];
return filter.name;
})
.flat(),
].forEach((key) => historySearch.delete(key));
for (const [name, value] of listSearch) {
historySearch.append(name, value);
}
history.replace({
search: '?' + historySearch.toString(),
});
},
),
reaction(
() => [model.filterQueryString, model.searchQueryString, model.sortQueryString],
() => {
model.scheduleRefresh();
model.setOffset(0);
},
),
reaction(() => model.pagingQueryString, model.scheduleRefresh),
];
model.scheduleRefresh(0);
};
const scheduleRefresh = (timeout = model.queryTimeout) => {
if (!getList) return;
clearTimeout(model.queryTimeoutId);
model.queryTimeoutId = setTimeout(getList, timeout);
};
const getStateFromSearch = () => {
model.resetFilters();
model.sorts = [];
const historySearch = new URLSearchParams(history.location.search);
if (historySearch.has('offset')) model.offset = Number(historySearch.get('offset'), 10);
if (historySearch.has('limit')) model.limit = Number(historySearch.get('limit'), 10);
if (historySearch.has(model.searchField)) {
quickSearch.changeSearchString(historySearch.get(model.searchField));
}
model.sorts = historySearch
.getAll('sort_by')
.map((item) => ({name: item.slice(1), direction: item.slice(0, 1)}));
model.filters.forEach((filter) => {
if (filter.type === 'date') {
filter.set([
historySearch.get(filter.prefix + 'from_dt'),
historySearch.get(filter.prefix + 'to_dt'),
]);
} else {
if (!historySearch.has(filter.name)) return;
historySearch.getAll(filter.name).forEach((item) => {
let val = item;
if (filter.type === 'boolean') {
val = val === 'true';
}
filter.toggleByValue(val);
});
}
});
};
const resetFilters = () => {
model.filters.forEach((filter) => filter.reset());
};
const setOffset = (offset) => {
model.offset = offset;
};
const setPageSize = (pageSize) => {
model.limit = pageSize;
};
const detach = () => {
model.historyDisposer();
model.historyDisposer = null;
model.reactionDisposers.forEach((dispose) => dispose());
model.offset = 0;
model.resetFilters();
quickSearch.reset();
};
const toggleSort = (name) => {
const target = model.sort[name];
if (target && target.direction === '+') {
model.sorts = model.sorts.filter((item) => item.name !== name);
} else if (target && target.direction === '-') {
target.direction = '+';
} else {
model.sorts.push({name, value: '-'});
}
};
return {
attach,
setOffset,
setPageSize,
getStateFromSearch,
scheduleRefresh,
detach,
resetFilters,
toggleSort,
};
})
.views((model) => ({
get filter() {
const filters = {};
model.filters.forEach((item) => {
filters[item.id] = item;
});
return filters;
},
get sort() {
const sorts = {};
model.sorts.forEach((item) => {
sorts[item.name] = item;
});
return sorts;
},
get filterQueryString() {
return model.filters
.map((item) => item.queryString)
.filter(Boolean)
.join('&');
},
get pagingQueryString() {
return `offset=${model.offset}&limit=${model.limit}`;
},
get searchQueryString() {
if (!model.searchField) return '';
const {searchString} = getRoot(model).quickSearch;
return searchString ? `${model.searchField}=${encodeURIComponent(searchString)}` : '';
},
get sortQueryString() {
return model.sorts.map((item) => `sort_by=${encodeURIComponent(item.direction)}${item.name}`).join('&');
},
get queryString() {
return [
model.pagingQueryString,
model.filterQueryString,
model.searchQueryString,
model.sortQueryString,
]
.filter(Boolean)
.join('&');
},
}));
return Model;
};
export default List;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment