import { flow, getRoot, types } from "mobx-state-tree";
|
import { guidGenerator } from "../../utils/random";
|
import { isDefined } from "../../utils/utils";
|
import { DEFAULT_PAGE_SIZE, getStoredPageSize } from "../../components/Common/Pagination/Pagination";
|
import { FF_LOPS_E_3, isFF } from "../../utils/feature-flags";
|
|
const listIncludes = (list, id) => {
|
const index = id !== undefined ? Array.from(list).findIndex((item) => item.id === id) : -1;
|
|
return index >= 0;
|
};
|
|
const MixinBase = types
|
.model("InfiniteListMixin", {
|
page: types.optional(types.integer, 0),
|
pageSize: types.optional(types.integer, getStoredPageSize("tasks", DEFAULT_PAGE_SIZE)),
|
total: types.optional(types.integer, 0),
|
loading: false,
|
loadingItem: false,
|
loadingItems: types.optional(types.array(types.number), []),
|
updated: guidGenerator(),
|
})
|
.views((self) => ({
|
get API() {
|
return self.root.API;
|
},
|
|
get root() {
|
return getRoot(self);
|
},
|
|
get totalPages() {
|
return Math.ceil(self.total / self.pageSize);
|
},
|
|
get hasNextPage() {
|
return self.page !== self.totalPages;
|
},
|
|
get isLoading() {
|
return self.loadingItem || self.loadingItems.length > 0;
|
},
|
|
get length() {
|
return self.list.length;
|
},
|
|
itemIsLoading(id) {
|
return self.loadingItems.includes(id);
|
},
|
}))
|
.actions((self) => ({
|
setSelected(val) {
|
let selected;
|
|
if (typeof val === "number") {
|
selected = self.list.find((t) => t.id === val);
|
if (!selected) {
|
selected = getRoot(self).taskStore.loadTask(val);
|
}
|
} else {
|
selected = val;
|
}
|
|
if (selected && selected.id !== self.selected?.id) {
|
self.selected = selected;
|
self.highlighted = selected;
|
|
getRoot(self).SDK.invoke("taskSelected");
|
}
|
},
|
|
hasRecord(id) {
|
return self.list.some((t) => t.id === Number(id));
|
},
|
|
unset({ withHightlight = false } = {}) {
|
self.selected = undefined;
|
if (withHightlight) self.highlighted = undefined;
|
},
|
|
setList({ list, total, reload, associatedList = [] }) {
|
const newEntity = list.map((t) => ({
|
...t,
|
source: JSON.stringify(t),
|
}));
|
|
self.total = total;
|
|
newEntity.forEach((n) => {
|
const index = self.list.findIndex((i) => i.id === n.id);
|
|
if (index >= 0) {
|
self.list.splice(index, 1);
|
}
|
});
|
|
if (reload) {
|
self.list = [...newEntity];
|
} else {
|
self.list.push(...newEntity);
|
}
|
|
self.associatedList = associatedList;
|
},
|
|
setLoading(id) {
|
if (id !== undefined) {
|
self.loadingItems.push(id);
|
} else {
|
self.loadingItem = true;
|
}
|
},
|
|
finishLoading(id) {
|
if (id !== undefined) {
|
self.loadingItems = self.loadingItems.filter((item) => item !== id);
|
} else {
|
self.loadingItem = false;
|
}
|
},
|
|
clear() {
|
self.highlighted = undefined;
|
self.list = [];
|
self.page = 0;
|
self.total = 0;
|
},
|
}));
|
|
export const DataStore = (modelName, { listItemType, apiMethod, properties, associatedItemType }) => {
|
const model = types
|
.model(modelName, {
|
...(properties ?? {}),
|
list: types.optional(types.array(listItemType), []),
|
selectedId: types.optional(types.maybeNull(types.number), null),
|
highlightedId: types.optional(types.maybeNull(types.number), null),
|
...(associatedItemType
|
? { associatedList: types.optional(types.maybeNull(types.array(associatedItemType)), []) }
|
: {}),
|
})
|
.views((self) => ({
|
get selected() {
|
return self.list.find(({ id }) => id === self.selectedId);
|
},
|
|
get highlighted() {
|
return self.list.find(({ id }) => id === self.highlightedId);
|
},
|
|
set selected(item) {
|
self.selectedId = item?.id ?? item;
|
},
|
|
set highlighted(item) {
|
self.highlightedId = item?.id ?? item;
|
},
|
}))
|
.volatile(() => ({
|
requestId: null,
|
debouncedFetch: null,
|
}))
|
.actions((self) => ({
|
updateItem(itemID, patch) {
|
let item = self.list.find((t) => t.id === itemID);
|
|
if (item) {
|
item.update(patch);
|
} else {
|
item = listItemType.create(patch);
|
self.list.push(item);
|
}
|
|
return item;
|
},
|
|
// Initialize debounced fetch function
|
initDebouncedFetch() {
|
if (!self.debouncedFetch) {
|
let timeoutId = null;
|
let pendingPromise = null;
|
|
self.debouncedFetch = (params) => {
|
return new Promise((resolve, reject) => {
|
// Clear any existing timeout
|
if (timeoutId) {
|
clearTimeout(timeoutId);
|
}
|
|
// Cancel any pending promise
|
if (pendingPromise) {
|
pendingPromise.cancel?.();
|
}
|
|
// Set new timeout
|
timeoutId = setTimeout(async () => {
|
try {
|
pendingPromise = self._performFetch(params);
|
const result = await pendingPromise;
|
resolve(result);
|
} catch (error) {
|
reject(error);
|
} finally {
|
pendingPromise = null;
|
}
|
}, 150);
|
});
|
};
|
}
|
},
|
|
// Internal fetch function that performs the actual API call
|
_performFetch: flow(function* ({ id, query, pageNumber = null, reload = false, interaction, pageSize } = {}) {
|
let currentViewId;
|
let currentViewQuery;
|
const requestId = (self.requestId = guidGenerator());
|
const root = getRoot(self);
|
|
if (id) {
|
currentViewId = id;
|
currentViewQuery = query;
|
} else {
|
const currentView = root.viewsStore.selected;
|
|
currentViewId = currentView?.id;
|
currentViewQuery = currentView?.virtual ? currentView?.query : null;
|
}
|
|
if (!isDefined(currentViewId)) return;
|
|
self.loading = true;
|
|
if (interaction === "filter" || interaction === "ordering" || reload) {
|
self.page = 1;
|
} else if (reload || isDefined(pageNumber)) {
|
if (self.page === 0) self.page = 1;
|
else if (isDefined(pageNumber)) self.page = pageNumber;
|
} else {
|
self.page++;
|
}
|
|
if (pageSize) {
|
self.pageSize = pageSize;
|
} else {
|
self.pageSize = getStoredPageSize("tasks", DEFAULT_PAGE_SIZE);
|
}
|
|
const params = {
|
page: self.page,
|
page_size: self.pageSize,
|
};
|
|
if (currentViewQuery) {
|
params.query = currentViewQuery;
|
} else {
|
params.view = currentViewId;
|
}
|
|
if (interaction) Object.assign(params, { interaction });
|
|
const data = yield root.apiCall(apiMethod, params, {}, { allowToCancel: root.SDK.type === "DE" });
|
|
// We cancel current request processing if request id
|
// changed during the request. It indicates that something
|
// triggered another request while current one is not yet finished
|
if (requestId !== self.requestId || data.isCanceled) {
|
console.log(`Request ${requestId} was cancelled by another request`);
|
return;
|
}
|
|
const highlightedID = self.highlighted;
|
const apiMethodSettings = root.API.getSettingsByMethodName(apiMethod);
|
const { total, [apiMethod]: list } = data;
|
let associatedList = [];
|
|
if (isFF(FF_LOPS_E_3) && apiMethodSettings?.associatedType) {
|
associatedList = data[apiMethodSettings?.associatedType];
|
}
|
|
if (list)
|
self.setList({
|
total,
|
list,
|
reload: reload || isDefined(pageNumber),
|
associatedList,
|
});
|
|
if (isDefined(highlightedID) && !listIncludes(self.list, highlightedID)) {
|
self.highlighted = null;
|
}
|
|
self.postProcessData?.(data);
|
|
self.loading = false;
|
|
root.SDK.invoke("dataFetched", self);
|
}),
|
|
// Public fetch function that uses debouncing
|
fetch({ id, query, pageNumber = null, reload = false, interaction, pageSize } = {}) {
|
const params = { id, query, pageNumber, reload, interaction, pageSize };
|
const root = getRoot(self);
|
// Only use debouncing for virtual tabs that use queries (like search/filter tabs)
|
const currentView = root.viewsStore.selected;
|
// const isVirtualTab = currentView?.virtual && currentView?.query;
|
|
// Initialize debounced function if not already done
|
self.initDebouncedFetch();
|
|
// For virtual tabs with queries, use debounced version
|
return self.debouncedFetch(params);
|
},
|
|
reload: flow(function* ({ id, query, interaction } = {}) {
|
yield self.fetch({ id, query, reload: true, interaction });
|
}),
|
|
focusPrev() {
|
const index = Math.max(0, self.list.indexOf(self.highlighted) - 1);
|
|
self.highlighted = self.list[index];
|
self.updated = guidGenerator();
|
|
return self.highlighted;
|
},
|
|
focusNext() {
|
const index = Math.min(self.list.length - 1, self.list.indexOf(self.highlighted) + 1);
|
|
self.highlighted = self.list[index];
|
self.updated = guidGenerator();
|
|
return self.highlighted;
|
},
|
}));
|
|
return types.compose(MixinBase, model);
|
};
|