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 @@
-
+
@@ -123,7 +123,7 @@