diff --git a/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md b/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md
index 05b4e508a..1323d0cba 100644
--- a/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md
+++ b/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md
@@ -1237,6 +1237,43 @@ If you want to make table header or pagination, you can add `makeHeaderSticky`,
>
```
+### Don't block pagination on loading
+
+Sometimes you might want to allow user switch between pages, even if old request wasn't finished. For these porpuses you can use `blockPaginationOnLoading` and `abortSignal` in data callback:
+```ts
+
+
+
+...
+
+async function loadPageData(data, abortSignal) {
+ const { offset, limit } = data;
+ // in real app do await callAdminForthApi or await fetch to get date, use offset and limit value to slice data
+ await new Promise(resolve => setTimeout(resolve, offset === 500)) // simulate network delay
+ if (abortSignal.abort) return; // since result won't be displayed, we stop computing
+
+ return {
+ data: [
+ { name: 'John', age: offset, country: 'US' },
+ { name: 'Rick', age: offset+1, country: 'CA' },
+ { name: 'Alice', age: offset+2, country: 'BR' },
+ ],
+ total: 30 // should return total amount of records in database
+ }
+}
+
+```
+
## ProgressBar
diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts
index 5725638f4..cdf506a07 100644
--- a/adminforth/modules/restApi.ts
+++ b/adminforth/modules/restApi.ts
@@ -155,6 +155,13 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
}
}
+ checkAbortSignal(abortSignal: AbortSignal): boolean {
+ if (abortSignal.aborted) {
+ return true;
+ }
+ return false;
+ }
+
registerEndpoints(server: IHttpServer) {
server.endpoint({
noAuth: true,
@@ -686,7 +693,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
server.endpoint({
method: 'POST',
path: '/get_resource_data',
- handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
+ handler: async ({ body, adminUser, headers, query, cookies, requestUrl, abortSignal }) => {
const { resourceId, source } = body;
if (['show', 'list', 'edit'].includes(source) === false) {
return { error: 'Invalid source, should be list or show' };
@@ -728,7 +735,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
if (!allowed) {
return { error };
}
-
+ if (this.checkAbortSignal(abortSignal)) { return { error: 'Request aborted' }; }
const hookSource = {
'show': 'show',
'list': 'list',
@@ -738,6 +745,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
for (const hook of listify(resource.hooks?.[hookSource]?.beforeDatasourceRequest as BeforeDataSourceRequestFunction[])) {
const filterTools = filtersTools.get(body);
body.filtersTools = filterTools;
+ if (this.checkAbortSignal(abortSignal)) { return { error: 'Request aborted' }; }
const resp = await (hook as BeforeDataSourceRequestFunction)({
resource,
query: body,
@@ -783,6 +791,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
throw new Error(`Wrong filter object value: ${JSON.stringify(filters)}`);
}
}
+ if (this.checkAbortSignal(abortSignal)) { return { error: 'Request aborted' }; }
const data = await this.adminforth.connectors[resource.dataSource].getData({
resource,
@@ -815,6 +824,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
if (pksUnique.length === 0) {
return;
}
+ if (this.checkAbortSignal(abortSignal)) { return { error: 'Request aborted' }; }
const targetData = await targetConnector.getData({
resource: targetResource,
limit: pksUnique.length,
@@ -859,6 +869,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
return;
}
});
+ if (this.checkAbortSignal(abortSignal)) { return { error: 'Request aborted' }; }
const targetData = (await Promise.all(Object.keys(pksUniques).map((polymorphicOnValue) =>
targetConnectors[polymorphicOnValue].getData({
@@ -939,6 +950,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
// only after adminforth made all post processing, give user ability to edit it
for (const hook of listify(resource.hooks?.[hookSource]?.afterDatasourceResponse)) {
+ if (this.checkAbortSignal(abortSignal)) { return { error: 'Request aborted' }; }
const resp = await hook({
resource,
query: body,
diff --git a/adminforth/servers/express.ts b/adminforth/servers/express.ts
index f0410000d..109ac3908 100644
--- a/adminforth/servers/express.ts
+++ b/adminforth/servers/express.ts
@@ -303,6 +303,12 @@ class ExpressServer implements IExpressHttpServer {
const fullPath = `${this.adminforth.config.baseUrl}/adminapi/v1${path}`;
const expressHandler = async (req, res) => {
+ const abortController = new AbortController();
+ res.on('close', () => {
+ if(req.destroyed) {
+ abortController.abort();
+ }
+ });
// Enforce JSON-only for mutation HTTP methods
// AdminForth API endpoints accept only application/json for POST, PUT, PATCH, DELETE
// If you need other content types, use a custom server endpoint.
@@ -357,7 +363,7 @@ class ExpressServer implements IExpressHttpServer {
const acceptLang = headers['accept-language'];
const tr = (msg: string, category: string, params: any, pluralizationNumber?: number): Promise => this.adminforth.tr(msg, category, acceptLang, params, pluralizationNumber);
- const input = { body, query, headers, cookies, adminUser, response, requestUrl, _raw_express_req: req, _raw_express_res: res, tr};
+ const input = { body, query, headers, cookies, adminUser, response, requestUrl, _raw_express_req: req, _raw_express_res: res, tr, abortSignal: abortController.signal};
let output;
try {
diff --git a/adminforth/spa/src/afcl/Table.vue b/adminforth/spa/src/afcl/Table.vue
index 13f14809e..ea553da3e 100644
--- a/adminforth/spa/src/afcl/Table.vue
+++ b/adminforth/spa/src/afcl/Table.vue
@@ -91,12 +91,12 @@