From f4c42ee363611e465a280f6d558df5199d793214 Mon Sep 17 00:00:00 2001 From: Yakov Zhmurov Date: Wed, 16 Oct 2024 18:44:37 +0300 Subject: [PATCH 1/5] DataSources - Cursors - implementation and tests --- .../__tests__/LazyListView.cursors.test.ts | 298 ++++++++++++++++++ .../src/data/processing/views/tree/ITree.ts | 4 + .../treeStructure/helpers/FetchingHelper.ts | 11 +- .../views/tree/treeStructure/types.ts | 5 + uui-core/src/data/querying/runDataQuery.ts | 7 +- uui-core/src/types/dataSources.ts | 6 + 6 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 uui-core/src/data/processing/views/__tests__/LazyListView.cursors.test.ts diff --git a/uui-core/src/data/processing/views/__tests__/LazyListView.cursors.test.ts b/uui-core/src/data/processing/views/__tests__/LazyListView.cursors.test.ts new file mode 100644 index 0000000000..cb6152246b --- /dev/null +++ b/uui-core/src/data/processing/views/__tests__/LazyListView.cursors.test.ts @@ -0,0 +1,298 @@ +import { LazyDataSource } from '../../LazyDataSource'; +import { delay, renderHook, waitFor } from '@epam/uui-test-utils'; + +import { + DataSourceState, LazyDataSourceApiRequest, DataQueryFilter, DataRowProps, IDataSourceView, +} from '../../../../types'; +import { runDataQuery } from '../../../querying/runDataQuery'; + +interface TestItem { + id: number; + parentId?: number; + childrenCount?: number; +} + +describe('LazyListView - cursors support', () => { + const testData: TestItem[] = [ + { id: 100 }, + { id: 110, parentId: 100 }, + { id: 200 }, + { id: 300 }, + { id: 310, parentId: 300 }, + { id: 320, parentId: 300 }, + { id: 330, parentId: 300 }, + { id: 340, parentId: 300 }, + { id: 350, parentId: 300 }, + { id: 360, parentId: 300 }, + { id: 400 }, + { id: 500 }, + { id: 600 }, + { id: 700 }, + { id: 800 }, + { id: 900 }, + ]; + + testData.forEach((i) => { + i.childrenCount = testData.filter((x) => x.parentId === i.id).length; + }); + + const testDataById = (Object as any).fromEntries(testData.map((i) => [i.id, i])); + + let currentValue: DataSourceState; + let onValueChanged = (newValue: React.SetStateAction, any>>) => { + if (typeof newValue === 'function') { + currentValue = newValue(currentValue); + return; + } + currentValue = newValue; + }; + + const testApi = jest.fn(async (rq: LazyDataSourceApiRequest>) => { + rq.sorting = [{ field: 'id', direction: 'asc' }]; + rq.range = { from: 0, count: rq.range?.count }; + + rq.filter = rq.filter ?? {}; + if (rq.cursor) { + rq.filter.id = { gt: rq.cursor }; + } + + const result = runDataQuery(testData, rq); + + if (result.items.length === rq.range.count!) { + result.cursor = result.items[result.items.length - 1].id; + } + + delete result.count; + + await delay(1); + + return result; + }); + + const treeDataSource = new LazyDataSource({ + api: (rq, ctx) => + ctx?.parent ? testApi({ ...rq, filter: { ...rq.filter, parentId: ctx.parentId } }) : testApi({ ...rq, filter: { ...rq.filter, parentId: { isNull: true } } }), + getChildCount: (i) => i.childrenCount, + getParentId: (i) => i.parentId, + }); + + beforeEach(() => { + currentValue = { topIndex: 0, visibleCount: 3 }; + onValueChanged = (newValue: React.SetStateAction, any>>) => { + if (typeof newValue === 'function') { + currentValue = newValue(currentValue); + return; + } + currentValue = newValue; + }; + testApi.mockClear(); + }); + + it('testApi is ok', async () => { + const data = await testApi({ filter: { parentId: 300 }, cursor: 310, range: { from: 3, count: 2 } }); + expect(data).toEqual({ + items: [testDataById[320], testDataById[330]], + cursor: 330, + }); + }); + + function expectViewToLookLike( + view: IDataSourceView>, + rows: Partial>[], + ) { + const viewRows = view.getVisibleRows(); + + rows.forEach((r) => { + if (r.id) { + r.value = testDataById[r.id]; + } + }); + + expect(viewRows).toEqual(rows.map((r) => expect.objectContaining(r))); + } + + it('can load list using cursors', async () => { + const hookResult = renderHook( + ({ value, onValueChange, props }) => treeDataSource.useView(value, onValueChange, props), + { initialProps: { value: currentValue, onValueChange: onValueChanged, props: {} } }, + ); + + let view = hookResult.result.current; + expectViewToLookLike(view, [ + { isLoading: true, depth: 0, indent: 0 }, + { isLoading: true, depth: 0, indent: 0 }, + { isLoading: true, depth: 0, indent: 0 }, + ]); + + await waitFor(() => { + expect(testApi).toBeCalledTimes(1); + }); + testApi.mockClear(); + + view = hookResult.result.current; + expectViewToLookLike(view, [ + { id: 100, isFoldable: true, isFolded: true }, + { id: 200, isFoldable: false }, + { id: 300, isFoldable: true, isFolded: true }, + ]); + + // Load more rows + + hookResult.rerender({ value: { ...currentValue, topIndex: 0, visibleCount: 6 }, onValueChange: onValueChanged, props: {} }); + view = hookResult.result.current; + + expectViewToLookLike(view, [ + { id: 100, isFoldable: true, isFolded: true }, + { id: 200, isFoldable: false }, + { id: 300, isFoldable: true, isFolded: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + ]); + + let listProps = view.getListProps(); + expect(listProps.knownRowsCount).toBe(6); + + await waitFor(() => { + expect(testApi).toBeCalledTimes(1); + }); + testApi.mockClear(); + + await waitFor(() => { + view = hookResult.result.current; + expectViewToLookLike(view, [ + { id: 100, isFoldable: true, isFolded: true }, + { id: 200, isFoldable: false }, + { id: 300, isFoldable: true, isFolded: true }, + { id: 400, isFoldable: false }, + { id: 500, isFoldable: false }, + { id: 600, isFoldable: false }, + ]); + }); + + listProps = view.getListProps(); + expect(listProps.knownRowsCount).toBe(6); + + // Load last rows + + hookResult.rerender({ value: { ...currentValue, topIndex: 5, visibleCount: 5 }, onValueChange: onValueChanged, props: {} }); + + view = hookResult.result.current; + + expectViewToLookLike(view, [ + { id: 600, isFoldable: false }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + ]); + + await waitFor(() => { + expect(testApi).toBeCalledTimes(1); + }); + testApi.mockClear(); + view = hookResult.result.current; + expectViewToLookLike(view, [ + { id: 600, isFoldable: false }, + { id: 700, isFoldable: false }, + { id: 800, isFoldable: false }, + { id: 900, isFoldable: false }, + ]); + + listProps = view.getListProps(); + expect(listProps.knownRowsCount).toBe(9); + }); + + it('can load child nodes with cursor', async () => { + currentValue.folded = { 300: false }; + currentValue.visibleCount = 5; + + const hookResult = renderHook( + ({ value, onValueChange, props }) => treeDataSource.useView(value, onValueChange, props), + { initialProps: { value: currentValue, onValueChange: onValueChanged, props: {} } }, + ); + + await waitFor(() => { + expect(testApi).toBeCalledTimes(2); + }); + testApi.mockClear(); + + hookResult.rerender({ value: { ...currentValue }, onValueChange: onValueChanged, props: {} }); + + let view = hookResult.result.current; + + expectViewToLookLike(view, [ + { id: 100, isFoldable: true, isFolded: true }, + { id: 200, isFoldable: false }, + { id: 300, isFoldable: true, isFolded: false }, + { id: 310, isFoldable: false, parentId: 300 }, + { id: 320, isFoldable: false, parentId: 300 }, + ]); + + hookResult.rerender({ value: { ...currentValue, topIndex: 5 }, onValueChange: onValueChanged, props: {} }); + + await waitFor(() => { + expect(testApi).toBeCalledTimes(2); + }); + testApi.mockClear(); + + view = hookResult.result.current; + + expectViewToLookLike(view, [ + { id: 330, isFoldable: false, parentId: 300 }, + { id: 340, isFoldable: false, parentId: 300 }, + { id: 350, isFoldable: false, parentId: 300 }, + { id: 360, isFoldable: false, parentId: 300 }, + { id: 400, isFoldable: false }, + ]); + }); + + it('works if end of the list happen to match batch boundaries', async () => { + currentValue.topIndex = 5; + currentValue.visibleCount = 4; + const hookResult = renderHook( + ({ value, onValueChange, props }) => treeDataSource.useView(value, onValueChange, props), + { initialProps: { value: currentValue, onValueChange: onValueChanged, props: {} } }, + ); + + await waitFor(() => { + expect(testApi).toBeCalledTimes(1); + }); + testApi.mockClear(); + + hookResult.rerender({ value: currentValue, onValueChange: onValueChanged, props: {} }); + + let view = hookResult.result.current; + expectViewToLookLike(view, [ + { id: 600, isFoldable: false }, + { id: 700, isFoldable: false }, + { id: 800, isFoldable: false }, + { id: 900, isFoldable: false }, + ]); + + // Scroll down. No more rows exists, and this should be handled correctly. + + currentValue.topIndex = 5; + currentValue.visibleCount = 5; + + hookResult.rerender({ value: currentValue, onValueChange: onValueChanged, props: {} }); + + await waitFor(() => { + expect(testApi).toBeCalledTimes(1); + }); + testApi.mockClear(); + + hookResult.rerender({ value: currentValue, onValueChange: onValueChanged, props: {} }); + + view = hookResult.result.current; + expectViewToLookLike(view, [ + { id: 600, isFoldable: false }, + { id: 700, isFoldable: false }, + { id: 800, isFoldable: false }, + { id: 900, isFoldable: false }, + ]); + + const listProps = view.getListProps(); + expect(listProps.knownRowsCount).toBe(9); + }); +}); diff --git a/uui-core/src/data/processing/views/tree/ITree.ts b/uui-core/src/data/processing/views/tree/ITree.ts index 7c1d3062c9..1f617a43d8 100644 --- a/uui-core/src/data/processing/views/tree/ITree.ts +++ b/uui-core/src/data/processing/views/tree/ITree.ts @@ -18,6 +18,10 @@ export interface ITreeItemsInfo extends ITreeNodeInfo { * ITree node loading/state status. */ status: ITreeNodeStatus; + /** + * TBD + */ + cursor?: any; } /** diff --git a/uui-core/src/data/processing/views/tree/treeStructure/helpers/FetchingHelper.ts b/uui-core/src/data/processing/views/tree/treeStructure/helpers/FetchingHelper.ts index 86904ba20f..1702e953eb 100644 --- a/uui-core/src/data/processing/views/tree/treeStructure/helpers/FetchingHelper.ts +++ b/uui-core/src/data/processing/views/tree/treeStructure/helpers/FetchingHelper.ts @@ -267,7 +267,13 @@ export class FetchingHelper { remainingRowsCount, loadAll, }: LoadItemsOptions) { - const { ids: originalIds, count: childrenCount, totalCount, assumedCount: prevAssumedCount } = tree.getItems(parentId); + const { + ids: originalIds, + count: childrenCount, + totalCount, + assumedCount: prevAssumedCount, + cursor: prevCursor, + } = tree.getItems(parentId); const inputIds = byParentId.has(parentId) ? byParentId.get(parentId) ?? originalIds : originalIds; let ids = inputIds ?? []; @@ -322,6 +328,7 @@ export class FetchingHelper { range, page: dataSourceState.page, pageSize: dataSourceState.pageSize, + cursor: prevCursor, }, requestContext, ); @@ -352,7 +359,7 @@ export class FetchingHelper { assumedCount = tree.getParams().getChildCount(parent); } - let nodeInfo = { count: childrenCount, totalCount, assumedCount: prevAssumedCount }; + let nodeInfo = { count: childrenCount, totalCount, assumedCount: prevAssumedCount, cursor: response.cursor }; if (newNodesCount !== childrenCount || assumedCount !== prevAssumedCount) { nodeInfo = { ...nodeInfo, count: newNodesCount, assumedCount }; } diff --git a/uui-core/src/data/processing/views/tree/treeStructure/types.ts b/uui-core/src/data/processing/views/tree/treeStructure/types.ts index 005c7e3ea3..c2563b3909 100644 --- a/uui-core/src/data/processing/views/tree/treeStructure/types.ts +++ b/uui-core/src/data/processing/views/tree/treeStructure/types.ts @@ -37,12 +37,17 @@ export interface ITreeNodeInfo { * If undefined, not all data is loaded from server. */ count?: number; + /** * Total count of the records. Usually, is returned from server on a root node fetch. */ totalCount?: number; + /** * Assumed count, got from the `getChildCount` result. */ assumedCount?: number; + + /** Last fetched cursor for this list */ + cursor?: any; } diff --git a/uui-core/src/data/querying/runDataQuery.ts b/uui-core/src/data/querying/runDataQuery.ts index c35fa901e5..f660c6ebc6 100644 --- a/uui-core/src/data/querying/runDataQuery.ts +++ b/uui-core/src/data/querying/runDataQuery.ts @@ -3,8 +3,13 @@ import { getOrderComparer } from './getOrderComparer'; import { getFilterPredicate } from './getFilterPredicate'; import { getSearchFilter } from './getSearchFilter'; import { orderBy } from '../../helpers'; +import { LazyDataSourceApiResponse } from '../../types/dataSources'; -export function runDataQuery(allItems: TItem[], request: DataQuery & { ids?: any[] }, searchBy?: (item: TItem) => string[]) { +export function runDataQuery( + allItems: TItem[], + request: DataQuery & { ids?: any[] }, + searchBy?: (item: TItem) => string[], +):LazyDataSourceApiResponse { let items = allItems || []; request = request || {}; diff --git a/uui-core/src/types/dataSources.ts b/uui-core/src/types/dataSources.ts index a7837499f0..54455c9a4a 100644 --- a/uui-core/src/types/dataSources.ts +++ b/uui-core/src/types/dataSources.ts @@ -420,6 +420,9 @@ export interface LazyDataSourceApiRequest { * Used for requesting specific items separately from the list. */ ids?: TId[]; + + /** TBD */ + cursor?: any; } /** Defines Lazy Data Source APIs response shape */ @@ -443,6 +446,9 @@ export interface LazyDataSourceApiResponse { * Total count of items which match current filter. */ totalCount?: number; + + /** TBD */ + cursor?: any; } /** Defines the context of API request. E.g. parent if we require to retrieve sub-list of the tree */ From 99e410d56fae574f6e920bc805598fe972395c49 Mon Sep 17 00:00:00 2001 From: Yakov Zhmurov Date: Wed, 16 Oct 2024 19:05:44 +0300 Subject: [PATCH 2/5] DataSources - Cursors - code doc --- uui-core/src/types/dataSources.ts | 39 +++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/uui-core/src/types/dataSources.ts b/uui-core/src/types/dataSources.ts index 54455c9a4a..7ae9a51c05 100644 --- a/uui-core/src/types/dataSources.ts +++ b/uui-core/src/types/dataSources.ts @@ -404,16 +404,22 @@ export interface LazyDataSourceApiRequest { * It is a merged result of filters from DataSourceState and LazyDataSourceProps. */ filter?: TFilter; + /** Sorting options, by which data should be sorted. */ sorting?: SortingOption[]; + /** The search string, by which data should be searched. */ search?: string; + /** Specifies a range of the rows to be retrieved. */ range?: LazyDataSourceApiRequestRange; + /** Page number for which data should be retrieved. */ page?: number; + /** Number of items at the page. */ pageSize?: number; + /** * An array of item IDs to be retrieved from the API. * Other request options like filter, search and others should be ignored when IDs are provided. @@ -421,7 +427,24 @@ export interface LazyDataSourceApiRequest { */ ids?: TId[]; - /** TBD */ + /** + * Cursor for cursor-based pagination. + * + * This property is used when implementing cursor-based pagination + * instead of traditional range-based pagination. + * + * The cursor is typically an opaque string or token that the server + * understands to determine the next set of items to return. + * + * If the previous API response includes a cursor, it will be passed in + * this property, to fetch more records. + * + * Note, that if you use cursors, you can ignore the range.from field. It would still have a + * valid value through - how many items are already fetched. + * + * You still need to account range.count, and return at least as much items as requested. + * Returning more items than range.count is supported. + */ cursor?: any; } @@ -447,7 +470,19 @@ export interface LazyDataSourceApiResponse { */ totalCount?: number; - /** TBD */ + /** + * Cursor for cursor-based pagination. + * + * The cursor is typically an opaque string or token that the server + * understands to determine the next set of items to return. + * + * You can pass cursor of the last item in the list, usually provided by the server. + * In this case, this cursor will be provided to the next request, allowing to fetch the next part of the list. + * + * The absence of cursor doesn't imply the end of the list. The end of the list + * is determined by comparing the number of items returned with the number requested: + * if fewer items than requested are returned, it implies that the end of the list has been reached. + */ cursor?: any; } From b7a4f1e2dfc1cf85c897d3f5747bc64afa13f946 Mon Sep 17 00:00:00 2001 From: Yakov Zhmurov Date: Thu, 17 Oct 2024 18:30:51 +0300 Subject: [PATCH 3/5] Fixes according to code-review --- .../processing/views/__tests__/LazyListView.cursors.test.ts | 2 +- uui-core/src/data/processing/views/tree/ITree.ts | 2 +- .../views/tree/treeStructure/helpers/FetchingHelper.ts | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/uui-core/src/data/processing/views/__tests__/LazyListView.cursors.test.ts b/uui-core/src/data/processing/views/__tests__/LazyListView.cursors.test.ts index cb6152246b..3624a21832 100644 --- a/uui-core/src/data/processing/views/__tests__/LazyListView.cursors.test.ts +++ b/uui-core/src/data/processing/views/__tests__/LazyListView.cursors.test.ts @@ -58,7 +58,7 @@ describe('LazyListView - cursors support', () => { const result = runDataQuery(testData, rq); - if (result.items.length === rq.range.count!) { + if (result.items.length > 0) { result.cursor = result.items[result.items.length - 1].id; } diff --git a/uui-core/src/data/processing/views/tree/ITree.ts b/uui-core/src/data/processing/views/tree/ITree.ts index 1f617a43d8..023c5e3037 100644 --- a/uui-core/src/data/processing/views/tree/ITree.ts +++ b/uui-core/src/data/processing/views/tree/ITree.ts @@ -19,7 +19,7 @@ export interface ITreeItemsInfo extends ITreeNodeInfo { */ status: ITreeNodeStatus; /** - * TBD + * Cursor to the last fetched item, if cursor-based pagination is used. */ cursor?: any; } diff --git a/uui-core/src/data/processing/views/tree/treeStructure/helpers/FetchingHelper.ts b/uui-core/src/data/processing/views/tree/treeStructure/helpers/FetchingHelper.ts index 1702e953eb..c165d316d7 100644 --- a/uui-core/src/data/processing/views/tree/treeStructure/helpers/FetchingHelper.ts +++ b/uui-core/src/data/processing/views/tree/treeStructure/helpers/FetchingHelper.ts @@ -107,7 +107,8 @@ export class FetchingHelper { if (ids !== currentIds || nodeInfo.count !== originalNodeInfo.count || nodeInfo.totalCount !== originalNodeInfo.totalCount - || nodeInfo.assumedCount !== originalNodeInfo.assumedCount) { + || nodeInfo.assumedCount !== originalNodeInfo.assumedCount + || nodeInfo.cursor !== originalNodeInfo.cursor) { nodeInfoById.set(parentId, nodeInfo); } @@ -301,7 +302,7 @@ export class FetchingHelper { if (missingCount === 0 || availableCount === 0 || skipRequest) { return { ids, - nodeInfo: { count: childrenCount, totalCount, assumedCount: prevAssumedCount }, + nodeInfo: { count: childrenCount, totalCount, assumedCount: prevAssumedCount, cursor: prevCursor }, loadedItems, }; } From cb94fcf390c47e1a5caa7134aaaa603452decaa5 Mon Sep 17 00:00:00 2001 From: Yakov Zhmurov Date: Thu, 17 Oct 2024 18:31:32 +0300 Subject: [PATCH 4/5] DataSources - Cursors - example and docs on site --- .../LazyDataSourceCursor.example.tsx | 53 ++ .../docs/dataSources/LazyDataSource.doc.tsx | 1 + ...ples-dataSources-LazyDataSourceCursor.json | 104 +++ public/docs/docsGenOutput/docsGenOutput.d.ts | 1 - public/docs/docsGenOutput/docsGenOutput.json | 602 ++++++++++-------- 5 files changed, 492 insertions(+), 269 deletions(-) create mode 100644 app/src/docs/_examples/dataSources/LazyDataSourceCursor.example.tsx create mode 100644 public/docs/content/examples-dataSources-LazyDataSourceCursor.json diff --git a/app/src/docs/_examples/dataSources/LazyDataSourceCursor.example.tsx b/app/src/docs/_examples/dataSources/LazyDataSourceCursor.example.tsx new file mode 100644 index 0000000000..35972e7449 --- /dev/null +++ b/app/src/docs/_examples/dataSources/LazyDataSourceCursor.example.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { DataQueryFilter, DataSourceState, useLazyDataSource, useUuiContext } from '@epam/uui-core'; +import { DataSourceViewer } from '@epam/uui-docs'; +import { TApi } from '../../../data'; +import { City } from '@epam/uui-docs'; + +export default function LazyDataSourceDataExample() { + const svc = useUuiContext(); + + const [value, onValueChange] = useState({}); + const dataSource = useLazyDataSource>({ + api: async (req) => { + // We emulate server cursor-based API here. + // Usually this done at server, and you need to pass cursor as is. + + // The server-side logic might be more complex, as we ignore several cases here: + // - we assume the list is sorted by name, and sorting can't change + // To handle this, cursor would need to store field by which the list is sorted. + // - we assume that names are unique. + // To handle this, we would need to add item ID to cursor, and make more complex filter, like: + // where (name > cursor.name) OR (name = cursor.name && id > cursor.id) + // order by name, id + + const { cursor, ...request } = req; + + if (cursor) { + request.filter = request.filter || {}; + // fetch only cities with name after the last fetched city alphabetically + request.filter.name = { gt: cursor }; + request.range = { ...request.range, from: 0 }; + } + + request.sorting = [{ field: 'name', direction: 'asc' }]; + + const response = await svc.api.demo.cities(request); + + if (response.items.length > 0) { + // store last item's name as cursor + response.cursor = response.items[response.items.length - 1].name; + } + + return response; + }, + }, []); + + return ( + + ); +} diff --git a/app/src/docs/dataSources/LazyDataSource.doc.tsx b/app/src/docs/dataSources/LazyDataSource.doc.tsx index caaf59d870..f01af03f32 100644 --- a/app/src/docs/dataSources/LazyDataSource.doc.tsx +++ b/app/src/docs/dataSources/LazyDataSource.doc.tsx @@ -20,6 +20,7 @@ export class DataSourcesLazyDataSourceDoc extends BaseDocsBlock { + ); } diff --git a/public/docs/content/examples-dataSources-LazyDataSourceCursor.json b/public/docs/content/examples-dataSources-LazyDataSourceCursor.json new file mode 100644 index 0000000000..2695d0fd78 --- /dev/null +++ b/public/docs/content/examples-dataSources-LazyDataSourceCursor.json @@ -0,0 +1,104 @@ +[ + { + "type": "paragraph", + "children": [ + { + "text": "Usually, backend uses integer ranges to allow querying part of the lists. E.g. " + }, + { + "text": "/api/items?from=10&count=20", + "uui-richTextEditor-code": true + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "With cursor-based pagination, backend serializes some state, associated with the next item in the list. Using this info, the next part of the list can be fetched more efficiently." + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "You can read more in this article: " + }, + { + "type": "link", + "url": "https://medium.com/@nimmikrishnab/cursor-based-pagination-37f5fae9f482", + "target": "_blank", + "children": [ + { + "text": "Cursor-based Pagination" + } + ] + }, + { + "text": "" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "To use this pattern, you need to return the latest cursor value in response to the " + }, + { + "text": "api", + "uui-richTextEditor-code": true + }, + { + "text": " callback. This cursor will be passed to the " + }, + { + "text": "api", + "uui-richTextEditor-code": true + }, + { + "text": " request, called to fetch the next part of this list." + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Note that:" + } + ] + }, + { + "type": "unordered-list", + "children": [ + { + "type": "list-item", + "children": [ + { + "type": "list-item-child", + "children": [ + { + "text": "absence of cursor in response doesn't imply the end of the list. End of the list is detected when there are fewer items returned, than was requested" + } + ] + } + ] + }, + { + "type": "list-item", + "children": [ + { + "type": "list-item-child", + "children": [ + { + "text": "with hierarchical lists, the root and all child sub-lists has its own cursor, and they are tracked separately." + } + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/public/docs/docsGenOutput/docsGenOutput.d.ts b/public/docs/docsGenOutput/docsGenOutput.d.ts index 53d1a2491d..df32e500aa 100644 --- a/public/docs/docsGenOutput/docsGenOutput.d.ts +++ b/public/docs/docsGenOutput/docsGenOutput.d.ts @@ -421,7 +421,6 @@ type Autogenerated_TDocsGenExportedTypeRef = '@epam/uui-core:AcceptDropParams' | '@epam/uui:TextModsOverride' | '@epam/uui:TextPlaceholderProps' | '@epam/uui:TextProps' | -'@epam/uui:TextSettings' | '@epam/uui:TimePickerBodyProps' | '@epam/uui:TimePickerModsOverride' | '@epam/uui:TimePickerProps' | diff --git a/public/docs/docsGenOutput/docsGenOutput.json b/public/docs/docsGenOutput/docsGenOutput.json index d3b28e43a6..2a82bd718a 100644 --- a/public/docs/docsGenOutput/docsGenOutput.json +++ b/public/docs/docsGenOutput/docsGenOutput.json @@ -1,5 +1,5 @@ { - "version": "5.9.0", + "version": "5.9.2", "docsGenTypes": { "@epam/uui-core:AcceptDropParams": { "summary": { @@ -20435,6 +20435,10 @@ " * ITree node loading/state status.", " */", " status: ITreeNodeStatus;", + " /**", + " * Cursor to the last fetched item, if cursor-based pagination is used.", + " */", + " cursor?: any;", "}" ] }, @@ -20474,6 +20478,19 @@ }, "required": true }, + { + "uid": "cursor", + "name": "cursor", + "comment": { + "raw": [ + "Cursor to the last fetched item, if cursor-based pagination is used." + ] + }, + "typeValue": { + "raw": "any" + }, + "required": false + }, { "uid": "count", "name": "count", @@ -20650,6 +20667,8 @@ " * Assumed count, got from the `getChildCount` result.", " */", " assumedCount?: number;", + " /** Last fetched cursor for this list */", + " cursor?: any;", "}" ] }, @@ -20702,6 +20721,19 @@ "type": "number" }, "required": false + }, + { + "uid": "cursor", + "name": "cursor", + "comment": { + "raw": [ + "Last fetched cursor for this list" + ] + }, + "typeValue": { + "raw": "any" + }, + "required": false } ], "propsFromUnion": false @@ -21356,6 +21388,25 @@ " * Used for requesting specific items separately from the list.", " */", " ids?: TId[];", + " /**", + " * Cursor for cursor-based pagination.", + " *", + " * This property is used when implementing cursor-based pagination", + " * instead of traditional range-based pagination.", + " *", + " * The cursor is typically an opaque string or token that the server", + " * understands to determine the next set of items to return.", + " *", + " * If the previous API response includes a cursor, it will be passed in", + " * this property, to fetch more records.", + " *", + " * Note, that if you use cursors, you can ignore the range.from field. It would still have a", + " * valid value through - how many items are already fetched.", + " *", + " * You still need to account range.count, and return at least as much items as requested.", + " * Returning more items than range.count is supported.", + " */", + " cursor?: any;", "}" ] }, @@ -21462,6 +21513,34 @@ "raw": "TId[]" }, "required": false + }, + { + "uid": "cursor", + "name": "cursor", + "comment": { + "raw": [ + "Cursor for cursor-based pagination.", + "", + " This property is used when implementing cursor-based pagination", + " instead of traditional range-based pagination.", + "", + " The cursor is typically an opaque string or token that the server", + " understands to determine the next set of items to return.", + "", + " If the previous API response includes a cursor, it will be passed in", + " this property, to fetch more records.", + "", + " Note, that if you use cursors, you can ignore the range.from field. It would still have a", + " valid value through - how many items are already fetched.", + "", + " You still need to account range.count, and return at least as much items as requested.", + " Returning more items than range.count is supported." + ] + }, + "typeValue": { + "raw": "any" + }, + "required": false } ], "propsFromUnion": false @@ -21636,6 +21715,20 @@ " * Total count of items which match current filter.", " */", " totalCount?: number;", + " /**", + " * Cursor for cursor-based pagination.", + " *", + " * The cursor is typically an opaque string or token that the server", + " * understands to determine the next set of items to return.", + " *", + " * You can pass cursor of the last item in the list, usually provided by the server.", + " * In this case, this cursor will be provided to the next request, allowing to fetch the next part of the list.", + " *", + " * The absence of cursor doesn't imply the end of the list. The end of the list", + " * is determined by comparing the number of items returned with the number requested:", + " * if fewer items than requested are returned, it implies that the end of the list has been reached.", + " */", + " cursor?: any;", "}" ] }, @@ -21702,6 +21795,29 @@ "type": "number" }, "required": false + }, + { + "uid": "cursor", + "name": "cursor", + "comment": { + "raw": [ + "Cursor for cursor-based pagination.", + "", + " The cursor is typically an opaque string or token that the server", + " understands to determine the next set of items to return.", + "", + " You can pass cursor of the last item in the list, usually provided by the server.", + " In this case, this cursor will be provided to the next request, allowing to fetch the next part of the list.", + "", + " The absence of cursor doesn't imply the end of the list. The end of the list", + " is determined by comparing the number of items returned with the number requested:", + " if fewer items than requested are returned, it implies that the end of the list has been reached." + ] + }, + "typeValue": { + "raw": "any" + }, + "required": false } ], "propsFromUnion": false @@ -34349,6 +34465,11 @@ " forceReload?: boolean;", " backgroundReload?: boolean;", " showSelectedOnly?: boolean;", + " /**", + " * Fetching function, which should be called if fetch is required.", + " * @param lazyLoadingAdvice - fetching advice.", + " */", + " onFetch?: (lazyLoadingAdvice: LazyFetchingAdvice) => void;", "}" ] }, @@ -34401,6 +34522,23 @@ "type": "bool" }, "required": false + }, + { + "uid": "onFetch", + "name": "onFetch", + "comment": { + "raw": [ + "Fetching function, which should be called if fetch is required.", + " @param lazyLoadingAdvice - fetching advice." + ] + }, + "typeValue": { + "raw": "(lazyLoadingAdvice: LazyFetchingAdvice) => void" + }, + "editor": { + "type": "func" + }, + "required": false } ], "propsFromUnion": false @@ -94419,7 +94557,11 @@ "typeValue": { "raw": "TextCoreProps", "print": [ - "interface TextCoreProps extends uuiComponents.TextProps, TextSettings {", + "interface TextCoreProps extends uuiComponents.TextProps {", + " /** Defines text line-height */", + " lineHeight?: '12' | '18' | '24' | '30';", + " /** Defines text font-size */", + " fontSize?: '10' | '12' | '14' | '16' | '18' | '24';", " /**", " * Defines text font weight value", " * @default '400'", @@ -94434,6 +94576,52 @@ ] }, "props": [ + { + "uid": "lineHeight", + "name": "lineHeight", + "comment": { + "raw": [ + "Defines text line-height" + ] + }, + "typeValue": { + "raw": "'12' | '18' | '24' | '30'" + }, + "editor": { + "type": "oneOf", + "options": [ + "12", + "18", + "24", + "30" + ] + }, + "required": false + }, + { + "uid": "fontSize", + "name": "fontSize", + "comment": { + "raw": [ + "Defines text font-size" + ] + }, + "typeValue": { + "raw": "'12' | '18' | '24' | '14' | '16' | '10'" + }, + "editor": { + "type": "oneOf", + "options": [ + "10", + "12", + "14", + "16", + "18", + "24" + ] + }, + "required": false + }, { "uid": "fontWeight", "name": "fontWeight", @@ -94575,54 +94763,6 @@ }, "from": "@epam/uui-core:IHasForwardedRef", "required": false - }, - { - "uid": "lineHeight", - "name": "lineHeight", - "comment": { - "raw": [ - "Defines text line-height" - ] - }, - "typeValue": { - "raw": "'12' | '18' | '24' | '30'" - }, - "editor": { - "type": "oneOf", - "options": [ - "12", - "18", - "24", - "30" - ] - }, - "from": "@epam/uui:TextSettings", - "required": false - }, - { - "uid": "fontSize", - "name": "fontSize", - "comment": { - "raw": [ - "Defines text font-size" - ] - }, - "typeValue": { - "raw": "'12' | '18' | '24' | '14' | '16' | '10'" - }, - "editor": { - "type": "oneOf", - "options": [ - "10", - "12", - "14", - "16", - "18", - "24" - ] - }, - "from": "@epam/uui:TextSettings", - "required": false } ], "propsFromUnion": false @@ -95453,6 +95593,54 @@ ] }, "props": [ + { + "uid": "lineHeight", + "name": "lineHeight", + "comment": { + "raw": [ + "Defines text line-height" + ] + }, + "typeValue": { + "raw": "'12' | '18' | '24' | '30'" + }, + "editor": { + "type": "oneOf", + "options": [ + "12", + "18", + "24", + "30" + ] + }, + "from": "@epam/uui:TextCoreProps", + "required": false + }, + { + "uid": "fontSize", + "name": "fontSize", + "comment": { + "raw": [ + "Defines text font-size" + ] + }, + "typeValue": { + "raw": "'12' | '18' | '24' | '14' | '16' | '10'" + }, + "editor": { + "type": "oneOf", + "options": [ + "10", + "12", + "14", + "16", + "18", + "24" + ] + }, + "from": "@epam/uui:TextCoreProps", + "required": false + }, { "uid": "fontWeight", "name": "fontWeight", @@ -95597,54 +95785,6 @@ "from": "@epam/uui-core:IHasForwardedRef", "required": false }, - { - "uid": "lineHeight", - "name": "lineHeight", - "comment": { - "raw": [ - "Defines text line-height" - ] - }, - "typeValue": { - "raw": "'12' | '18' | '24' | '30'" - }, - "editor": { - "type": "oneOf", - "options": [ - "12", - "18", - "24", - "30" - ] - }, - "from": "@epam/uui:TextSettings", - "required": false - }, - { - "uid": "fontSize", - "name": "fontSize", - "comment": { - "raw": [ - "Defines text font-size" - ] - }, - "typeValue": { - "raw": "'12' | '18' | '24' | '14' | '16' | '10'" - }, - "editor": { - "type": "oneOf", - "options": [ - "10", - "12", - "14", - "16", - "18", - "24" - ] - }, - "from": "@epam/uui:TextSettings", - "required": false - }, { "uid": "color", "name": "color", @@ -95711,80 +95851,6 @@ "propsFromUnion": false } }, - "@epam/uui:TextSettings": { - "summary": { - "module": "@epam/uui", - "typeName": { - "name": "TextSettings", - "nameFull": "TextSettings" - }, - "src": "uui/helpers/textLayout.tsx", - "exported": true - }, - "details": { - "kind": 264, - "typeValue": { - "raw": "TextSettings", - "print": [ - "interface TextSettings {", - " /** Defines text line-height */", - " lineHeight?: '12' | '18' | '24' | '30';", - " /** Defines text font-size */", - " fontSize?: '10' | '12' | '14' | '16' | '18' | '24';", - "}" - ] - }, - "props": [ - { - "uid": "lineHeight", - "name": "lineHeight", - "comment": { - "raw": [ - "Defines text line-height" - ] - }, - "typeValue": { - "raw": "'12' | '18' | '24' | '30'" - }, - "editor": { - "type": "oneOf", - "options": [ - "12", - "18", - "24", - "30" - ] - }, - "required": false - }, - { - "uid": "fontSize", - "name": "fontSize", - "comment": { - "raw": [ - "Defines text font-size" - ] - }, - "typeValue": { - "raw": "'12' | '18' | '24' | '14' | '16' | '10'" - }, - "editor": { - "type": "oneOf", - "options": [ - "10", - "12", - "14", - "16", - "18", - "24" - ] - }, - "required": false - } - ], - "propsFromUnion": false - } - }, "@epam/uui:TimePickerBodyProps": { "summary": { "module": "@epam/uui", @@ -102596,6 +102662,54 @@ ] }, "props": [ + { + "uid": "lineHeight", + "name": "lineHeight", + "comment": { + "raw": [ + "Defines text line-height" + ] + }, + "typeValue": { + "raw": "'18' | '24' | '30' | '12'" + }, + "editor": { + "type": "oneOf", + "options": [ + "12", + "18", + "24", + "30" + ] + }, + "from": "@epam/uui:TextCoreProps", + "required": false + }, + { + "uid": "fontSize", + "name": "fontSize", + "comment": { + "raw": [ + "Defines text font-size" + ] + }, + "typeValue": { + "raw": "'18' | '24' | '12' | '10' | '14' | '16'" + }, + "editor": { + "type": "oneOf", + "options": [ + "10", + "12", + "14", + "16", + "18", + "24" + ] + }, + "from": "@epam/uui:TextCoreProps", + "required": false + }, { "uid": "fontWeight", "name": "fontWeight", @@ -102740,54 +102854,6 @@ "from": "@epam/uui-core:IHasForwardedRef", "required": false }, - { - "uid": "lineHeight", - "name": "lineHeight", - "comment": { - "raw": [ - "Defines text line-height" - ] - }, - "typeValue": { - "raw": "'18' | '24' | '30' | '12'" - }, - "editor": { - "type": "oneOf", - "options": [ - "12", - "18", - "24", - "30" - ] - }, - "from": "@epam/uui:TextSettings", - "required": false - }, - { - "uid": "fontSize", - "name": "fontSize", - "comment": { - "raw": [ - "Defines text font-size" - ] - }, - "typeValue": { - "raw": "'18' | '24' | '12' | '10' | '14' | '16'" - }, - "editor": { - "type": "oneOf", - "options": [ - "10", - "12", - "14", - "16", - "18", - "24" - ] - }, - "from": "@epam/uui:TextSettings", - "required": false - }, { "uid": "color", "name": "color", @@ -109889,6 +109955,54 @@ ] }, "props": [ + { + "uid": "lineHeight", + "name": "lineHeight", + "comment": { + "raw": [ + "Defines text line-height" + ] + }, + "typeValue": { + "raw": "'18' | '24' | '30' | '12'" + }, + "editor": { + "type": "oneOf", + "options": [ + "12", + "18", + "24", + "30" + ] + }, + "from": "@epam/uui:TextCoreProps", + "required": false + }, + { + "uid": "fontSize", + "name": "fontSize", + "comment": { + "raw": [ + "Defines text font-size" + ] + }, + "typeValue": { + "raw": "'18' | '24' | '12' | '10' | '14' | '16'" + }, + "editor": { + "type": "oneOf", + "options": [ + "10", + "12", + "14", + "16", + "18", + "24" + ] + }, + "from": "@epam/uui:TextCoreProps", + "required": false + }, { "uid": "fontWeight", "name": "fontWeight", @@ -110033,54 +110147,6 @@ "from": "@epam/uui-core:IHasForwardedRef", "required": false }, - { - "uid": "lineHeight", - "name": "lineHeight", - "comment": { - "raw": [ - "Defines text line-height" - ] - }, - "typeValue": { - "raw": "'18' | '24' | '30' | '12'" - }, - "editor": { - "type": "oneOf", - "options": [ - "12", - "18", - "24", - "30" - ] - }, - "from": "@epam/uui:TextSettings", - "required": false - }, - { - "uid": "fontSize", - "name": "fontSize", - "comment": { - "raw": [ - "Defines text font-size" - ] - }, - "typeValue": { - "raw": "'18' | '24' | '12' | '10' | '14' | '16'" - }, - "editor": { - "type": "oneOf", - "options": [ - "10", - "12", - "14", - "16", - "18", - "24" - ] - }, - "from": "@epam/uui:TextSettings", - "required": false - }, { "uid": "color", "name": "color", From c8261e89ef351eeb033b04aa47e6c57e379db5b7 Mon Sep 17 00:00:00 2001 From: Yakov Zhmurov Date: Thu, 17 Oct 2024 18:34:00 +0300 Subject: [PATCH 5/5] DataSources - Cursors - change log --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 3baa3983ea..d43e5a7670 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ **What's New** * Sass updated to the last version, warnings 'Mixed Declarations' fixed https://sass-lang.com/documentation/breaking-changes/mixed-decls/ +* Data Sources: cursor-based pagination support. More details [here](http://uui.epam.com/documents?id=dataSources-lazy-dataSource&mode=doc&category=dataSources&theme=loveship#using_cursor-based_pagination) **What's Fixed**