@webiny/app-admin 6.5.0-beta.0 → 6.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +19 -19
- package/presentation/listPresenter/FolderAwareDataSource.d.ts +5 -0
- package/presentation/listPresenter/FolderAwareDataSource.js +4 -3
- package/presentation/listPresenter/FolderAwareDataSource.js.map +1 -1
- package/presentation/listPresenter/QueryMatcher.d.ts +28 -5
- package/presentation/listPresenter/QueryMatcher.js +72 -2
- package/presentation/listPresenter/QueryMatcher.js.map +1 -1
- package/presentation/listPresenter/QueryMatcher.test.d.ts +1 -0
- package/presentation/listPresenter/QueryMatcher.test.js +345 -0
- package/presentation/listPresenter/QueryMatcher.test.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webiny/app-admin",
|
|
3
|
-
"version": "6.5.0-beta.
|
|
3
|
+
"version": "6.5.0-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./index.js",
|
|
@@ -26,24 +26,24 @@
|
|
|
26
26
|
"@iconify/json": "2.2.503",
|
|
27
27
|
"@lexical/utils": "0.48.0",
|
|
28
28
|
"@types/react": "18.3.31",
|
|
29
|
-
"@webiny/admin-ui": "6.5.0-beta.
|
|
30
|
-
"@webiny/app": "6.5.0-beta.
|
|
29
|
+
"@webiny/admin-ui": "6.5.0-beta.1",
|
|
30
|
+
"@webiny/app": "6.5.0-beta.1",
|
|
31
31
|
"@webiny/di": "1.0.2",
|
|
32
|
-
"@webiny/feature": "6.5.0-beta.
|
|
33
|
-
"@webiny/form": "6.5.0-beta.
|
|
34
|
-
"@webiny/icons": "6.5.0-beta.
|
|
35
|
-
"@webiny/lexical-converter": "6.5.0-beta.
|
|
36
|
-
"@webiny/lexical-editor": "6.5.0-beta.
|
|
37
|
-
"@webiny/lexical-nodes": "6.5.0-beta.
|
|
38
|
-
"@webiny/lexical-theme": "6.5.0-beta.
|
|
39
|
-
"@webiny/plugins": "6.5.0-beta.
|
|
40
|
-
"@webiny/react-composition": "6.5.0-beta.
|
|
41
|
-
"@webiny/react-properties": "6.5.0-beta.
|
|
42
|
-
"@webiny/sdk": "6.5.0-beta.
|
|
43
|
-
"@webiny/telemetry": "6.5.0-beta.
|
|
44
|
-
"@webiny/utils": "6.5.0-beta.
|
|
45
|
-
"@webiny/validation": "6.5.0-beta.
|
|
46
|
-
"@webiny/wcp": "6.5.0-beta.
|
|
32
|
+
"@webiny/feature": "6.5.0-beta.1",
|
|
33
|
+
"@webiny/form": "6.5.0-beta.1",
|
|
34
|
+
"@webiny/icons": "6.5.0-beta.1",
|
|
35
|
+
"@webiny/lexical-converter": "6.5.0-beta.1",
|
|
36
|
+
"@webiny/lexical-editor": "6.5.0-beta.1",
|
|
37
|
+
"@webiny/lexical-nodes": "6.5.0-beta.1",
|
|
38
|
+
"@webiny/lexical-theme": "6.5.0-beta.1",
|
|
39
|
+
"@webiny/plugins": "6.5.0-beta.1",
|
|
40
|
+
"@webiny/react-composition": "6.5.0-beta.1",
|
|
41
|
+
"@webiny/react-properties": "6.5.0-beta.1",
|
|
42
|
+
"@webiny/sdk": "6.5.0-beta.1",
|
|
43
|
+
"@webiny/telemetry": "6.5.0-beta.1",
|
|
44
|
+
"@webiny/utils": "6.5.0-beta.1",
|
|
45
|
+
"@webiny/validation": "6.5.0-beta.1",
|
|
46
|
+
"@webiny/wcp": "6.5.0-beta.1",
|
|
47
47
|
"apollo-cache": "1.3.5",
|
|
48
48
|
"apollo-client": "2.6.10",
|
|
49
49
|
"apollo-link": "1.2.14",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@types/react-transition-group": "4.4.12",
|
|
84
84
|
"@types/store": "2.0.5",
|
|
85
85
|
"@types/tinycolor2": "1.4.6",
|
|
86
|
-
"@webiny/build-tools": "6.5.0-beta.
|
|
86
|
+
"@webiny/build-tools": "6.5.0-beta.1",
|
|
87
87
|
"rimraf": "6.1.3",
|
|
88
88
|
"typescript": "7.0.2",
|
|
89
89
|
"vitest": "4.1.10"
|
|
@@ -10,6 +10,11 @@ export interface FolderAwareDataSourceConfig<TRow> {
|
|
|
10
10
|
};
|
|
11
11
|
keyField: keyof TRow & string;
|
|
12
12
|
localFilters?: Record<string, (item: TRow, value: unknown) => boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Reads the value of a sort field from an item, for ordering rows locally.
|
|
15
|
+
* See `QueryMatcherConfig.getSortValue`.
|
|
16
|
+
*/
|
|
17
|
+
getSortValue?: (item: TRow, field: string) => unknown;
|
|
13
18
|
}
|
|
14
19
|
export interface FetchParams {
|
|
15
20
|
where?: Record<string, unknown>;
|
|
@@ -28,7 +28,8 @@ class FolderAwareDataSource {
|
|
|
28
28
|
};
|
|
29
29
|
this.queryMatcher = new QueryMatcher({
|
|
30
30
|
keyField: config.keyField,
|
|
31
|
-
localFilters
|
|
31
|
+
localFilters,
|
|
32
|
+
getSortValue: config.getSortValue
|
|
32
33
|
});
|
|
33
34
|
makeObservable(this, {
|
|
34
35
|
_meta: observable,
|
|
@@ -59,7 +60,7 @@ class FolderAwareDataSource {
|
|
|
59
60
|
});
|
|
60
61
|
runInAction(()=>{
|
|
61
62
|
this.onQueryResult(result.data);
|
|
62
|
-
this.queryMatcher.updateFromQuery(params, result.data.map((item)=>String(item[this._keyField])));
|
|
63
|
+
this.queryMatcher.updateFromQuery(params, result.data.map((item)=>String(item[this._keyField])), result.meta.hasMoreItems);
|
|
63
64
|
this._meta = {
|
|
64
65
|
cursor: result.meta.cursor,
|
|
65
66
|
hasMoreItems: result.meta.hasMoreItems,
|
|
@@ -85,7 +86,7 @@ class FolderAwareDataSource {
|
|
|
85
86
|
});
|
|
86
87
|
runInAction(()=>{
|
|
87
88
|
this.onLoadMoreResult(result.data);
|
|
88
|
-
this.queryMatcher.appendResultKeys(result.data.map((item)=>String(item[this._keyField])));
|
|
89
|
+
this.queryMatcher.appendResultKeys(result.data.map((item)=>String(item[this._keyField])), result.meta.hasMoreItems);
|
|
89
90
|
this._meta = {
|
|
90
91
|
cursor: result.meta.cursor,
|
|
91
92
|
hasMoreItems: result.meta.hasMoreItems,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presentation/listPresenter/FolderAwareDataSource.js","sources":["../../../src/presentation/listPresenter/FolderAwareDataSource.ts"],"sourcesContent":["import { makeObservable, observable, computed, runInAction, action } from \"mobx\";\nimport type { IDataSource, IDataSourceQuery, IDataSourceMeta } from \"./abstractions.js\";\nimport { QueryMatcher } from \"./QueryMatcher.js\";\n\nexport interface FolderAwareDataSourceConfig<TRow> {\n locationField?: string;\n registeredFilterNames?: string[];\n getDescendantFolders?: { execute(folderId: string): { id: string }[] };\n keyField: keyof TRow & string;\n localFilters?: Record<string, (item: TRow, value: unknown) => boolean>;\n}\n\nexport interface FetchParams {\n where?: Record<string, unknown>;\n sort?: string[];\n search?: string;\n limit?: number;\n after?: string;\n}\n\nexport interface FetchResult<TRow> {\n data: TRow[];\n meta: { cursor: string | null; hasMoreItems: boolean; totalCount: number };\n}\n\nexport abstract class FolderAwareDataSource<TRow> implements IDataSource<TRow> {\n private _meta: IDataSourceMeta = { cursor: null, hasMoreItems: false, totalCount: 0 };\n private _loading = false;\n private _locationField: string;\n private _registeredFilterNames: Set<string>;\n private _getDescendantFolders: FolderAwareDataSourceConfig<TRow>[\"getDescendantFolders\"];\n private _keyField: keyof TRow & string;\n protected queryMatcher: QueryMatcher<TRow>;\n\n constructor(config: FolderAwareDataSourceConfig<TRow>) {\n this._locationField = config.locationField ?? \"location\";\n this._registeredFilterNames = new Set([\n ...(config.registeredFilterNames ?? []),\n ...Object.keys(config.localFilters ?? {})\n ]);\n this._getDescendantFolders = config.getDescendantFolders;\n this._keyField = config.keyField;\n\n const locationField = this._locationField;\n const localFilters: Record<string, (item: TRow, value: unknown) => boolean> = {\n folderId: (item, value) => {\n const folderId = value as string;\n const location = (item as Record<string, any>)[locationField];\n const itemFolderId = location?.folderId as string | undefined;\n if (folderId && folderId !== \"root\") {\n return itemFolderId === folderId;\n }\n return !itemFolderId || itemFolderId === \"root\";\n },\n ...config.localFilters\n };\n\n this.queryMatcher = new QueryMatcher<TRow>({\n keyField: config.keyField,\n localFilters\n });\n\n makeObservable<FolderAwareDataSource<TRow>, \"_meta\" | \"_loading\">(this, {\n _meta: observable,\n _loading: observable,\n meta: computed,\n loading: computed,\n query: action,\n loadMore: action\n });\n }\n\n abstract get rows(): TRow[];\n\n abstract fetch(params: FetchParams): Promise<FetchResult<TRow>>;\n\n protected onQueryResult(_data: TRow[]): void {}\n\n protected onLoadMoreResult(_data: TRow[]): void {}\n\n get meta(): IDataSourceMeta {\n return this._meta;\n }\n\n get loading(): boolean {\n return this._loading;\n }\n\n async query(params: IDataSourceQuery): Promise<void> {\n this._loading = true;\n\n try {\n const result = await this.fetch({\n where: this.buildWhere(params),\n sort: this.buildSort(params),\n search: params.search,\n limit: params.limit,\n after: params.cursor\n });\n\n runInAction(() => {\n this.onQueryResult(result.data);\n this.queryMatcher.updateFromQuery(\n params,\n result.data.map(item => String(item[this._keyField]))\n );\n this._meta = {\n cursor: result.meta.cursor,\n hasMoreItems: result.meta.hasMoreItems,\n totalCount: result.meta.totalCount\n };\n });\n } finally {\n runInAction(() => {\n this._loading = false;\n });\n }\n }\n\n async loadMore(params: IDataSourceQuery): Promise<void> {\n if (!this._meta.hasMoreItems || this._loading) {\n return;\n }\n\n this._loading = true;\n\n try {\n const result = await this.fetch({\n where: this.buildWhere(params),\n sort: this.buildSort(params),\n search: params.search,\n limit: params.limit,\n after: this._meta.cursor ?? undefined\n });\n\n runInAction(() => {\n this.onLoadMoreResult(result.data);\n this.queryMatcher.appendResultKeys(\n result.data.map(item => String(item[this._keyField]))\n );\n this._meta = {\n cursor: result.meta.cursor,\n hasMoreItems: result.meta.hasMoreItems,\n totalCount: result.meta.totalCount\n };\n });\n } finally {\n runInAction(() => {\n this._loading = false;\n });\n }\n }\n\n protected customizeWhere(\n _where: Record<string, unknown>,\n _filters: Record<string, unknown>\n ): void {}\n\n protected shouldExpandFolders(_filters: Record<string, unknown>): boolean {\n return false;\n }\n\n private buildWhere(params: IDataSourceQuery): Record<string, unknown> | undefined {\n const where: Record<string, unknown> = {};\n const filters = params.filters ?? {};\n const folderId = (filters.folderId as string | undefined) || \"root\";\n const isRoot = folderId === \"root\";\n const hasAdvancedFilters = Object.keys(filters).some(\n k => k !== \"folderId\" && !this._registeredFilterNames.has(k)\n );\n const isSearching =\n !!params.search || hasAdvancedFilters || this.shouldExpandFolders(filters);\n\n if (isSearching && isRoot) {\n // Search/filter from root: no folder filter — search all folders.\n } else if (isSearching && this._getDescendantFolders) {\n const descendants = this._getDescendantFolders.execute(folderId);\n const folderIds = descendants.map(f => f.id);\n where[this._locationField] = { folderId_in: folderIds };\n } else {\n where[this._locationField] = { folderId };\n }\n\n for (const [key, value] of Object.entries(filters)) {\n if (key === \"folderId\") {\n continue;\n }\n where[key] = value;\n }\n\n this.customizeWhere(where, filters);\n\n return Object.keys(where).length > 0 ? where : undefined;\n }\n\n private buildSort(params: IDataSourceQuery): string[] | undefined {\n if (!params.sort) {\n return undefined;\n }\n return [`${params.sort.field}_${params.sort.direction}`];\n }\n}\n"],"names":["FolderAwareDataSource","config","Set","Object","locationField","localFilters","item","value","folderId","location","itemFolderId","QueryMatcher","makeObservable","observable","computed","action","_data","params","result","runInAction","String","undefined","_where","_filters","where","filters","isRoot","hasAdvancedFilters","k","isSearching","descendants","folderIds","f","key"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"presentation/listPresenter/FolderAwareDataSource.js","sources":["../../../src/presentation/listPresenter/FolderAwareDataSource.ts"],"sourcesContent":["import { makeObservable, observable, computed, runInAction, action } from \"mobx\";\nimport type { IDataSource, IDataSourceQuery, IDataSourceMeta } from \"./abstractions.js\";\nimport { QueryMatcher } from \"./QueryMatcher.js\";\n\nexport interface FolderAwareDataSourceConfig<TRow> {\n locationField?: string;\n registeredFilterNames?: string[];\n getDescendantFolders?: { execute(folderId: string): { id: string }[] };\n keyField: keyof TRow & string;\n localFilters?: Record<string, (item: TRow, value: unknown) => boolean>;\n /**\n * Reads the value of a sort field from an item, for ordering rows locally.\n * See `QueryMatcherConfig.getSortValue`.\n */\n getSortValue?: (item: TRow, field: string) => unknown;\n}\n\nexport interface FetchParams {\n where?: Record<string, unknown>;\n sort?: string[];\n search?: string;\n limit?: number;\n after?: string;\n}\n\nexport interface FetchResult<TRow> {\n data: TRow[];\n meta: { cursor: string | null; hasMoreItems: boolean; totalCount: number };\n}\n\nexport abstract class FolderAwareDataSource<TRow> implements IDataSource<TRow> {\n private _meta: IDataSourceMeta = { cursor: null, hasMoreItems: false, totalCount: 0 };\n private _loading = false;\n private _locationField: string;\n private _registeredFilterNames: Set<string>;\n private _getDescendantFolders: FolderAwareDataSourceConfig<TRow>[\"getDescendantFolders\"];\n private _keyField: keyof TRow & string;\n protected queryMatcher: QueryMatcher<TRow>;\n\n constructor(config: FolderAwareDataSourceConfig<TRow>) {\n this._locationField = config.locationField ?? \"location\";\n this._registeredFilterNames = new Set([\n ...(config.registeredFilterNames ?? []),\n ...Object.keys(config.localFilters ?? {})\n ]);\n this._getDescendantFolders = config.getDescendantFolders;\n this._keyField = config.keyField;\n\n const locationField = this._locationField;\n const localFilters: Record<string, (item: TRow, value: unknown) => boolean> = {\n folderId: (item, value) => {\n const folderId = value as string;\n const location = (item as Record<string, any>)[locationField];\n const itemFolderId = location?.folderId as string | undefined;\n if (folderId && folderId !== \"root\") {\n return itemFolderId === folderId;\n }\n return !itemFolderId || itemFolderId === \"root\";\n },\n ...config.localFilters\n };\n\n this.queryMatcher = new QueryMatcher<TRow>({\n keyField: config.keyField,\n localFilters,\n getSortValue: config.getSortValue\n });\n\n makeObservable<FolderAwareDataSource<TRow>, \"_meta\" | \"_loading\">(this, {\n _meta: observable,\n _loading: observable,\n meta: computed,\n loading: computed,\n query: action,\n loadMore: action\n });\n }\n\n abstract get rows(): TRow[];\n\n abstract fetch(params: FetchParams): Promise<FetchResult<TRow>>;\n\n protected onQueryResult(_data: TRow[]): void {}\n\n protected onLoadMoreResult(_data: TRow[]): void {}\n\n get meta(): IDataSourceMeta {\n return this._meta;\n }\n\n get loading(): boolean {\n return this._loading;\n }\n\n async query(params: IDataSourceQuery): Promise<void> {\n this._loading = true;\n\n try {\n const result = await this.fetch({\n where: this.buildWhere(params),\n sort: this.buildSort(params),\n search: params.search,\n limit: params.limit,\n after: params.cursor\n });\n\n runInAction(() => {\n this.onQueryResult(result.data);\n this.queryMatcher.updateFromQuery(\n params,\n result.data.map(item => String(item[this._keyField])),\n result.meta.hasMoreItems\n );\n this._meta = {\n cursor: result.meta.cursor,\n hasMoreItems: result.meta.hasMoreItems,\n totalCount: result.meta.totalCount\n };\n });\n } finally {\n runInAction(() => {\n this._loading = false;\n });\n }\n }\n\n async loadMore(params: IDataSourceQuery): Promise<void> {\n if (!this._meta.hasMoreItems || this._loading) {\n return;\n }\n\n this._loading = true;\n\n try {\n const result = await this.fetch({\n where: this.buildWhere(params),\n sort: this.buildSort(params),\n search: params.search,\n limit: params.limit,\n after: this._meta.cursor ?? undefined\n });\n\n runInAction(() => {\n this.onLoadMoreResult(result.data);\n this.queryMatcher.appendResultKeys(\n result.data.map(item => String(item[this._keyField])),\n result.meta.hasMoreItems\n );\n this._meta = {\n cursor: result.meta.cursor,\n hasMoreItems: result.meta.hasMoreItems,\n totalCount: result.meta.totalCount\n };\n });\n } finally {\n runInAction(() => {\n this._loading = false;\n });\n }\n }\n\n protected customizeWhere(\n _where: Record<string, unknown>,\n _filters: Record<string, unknown>\n ): void {}\n\n protected shouldExpandFolders(_filters: Record<string, unknown>): boolean {\n return false;\n }\n\n private buildWhere(params: IDataSourceQuery): Record<string, unknown> | undefined {\n const where: Record<string, unknown> = {};\n const filters = params.filters ?? {};\n const folderId = (filters.folderId as string | undefined) || \"root\";\n const isRoot = folderId === \"root\";\n const hasAdvancedFilters = Object.keys(filters).some(\n k => k !== \"folderId\" && !this._registeredFilterNames.has(k)\n );\n const isSearching =\n !!params.search || hasAdvancedFilters || this.shouldExpandFolders(filters);\n\n if (isSearching && isRoot) {\n // Search/filter from root: no folder filter — search all folders.\n } else if (isSearching && this._getDescendantFolders) {\n const descendants = this._getDescendantFolders.execute(folderId);\n const folderIds = descendants.map(f => f.id);\n where[this._locationField] = { folderId_in: folderIds };\n } else {\n where[this._locationField] = { folderId };\n }\n\n for (const [key, value] of Object.entries(filters)) {\n if (key === \"folderId\") {\n continue;\n }\n where[key] = value;\n }\n\n this.customizeWhere(where, filters);\n\n return Object.keys(where).length > 0 ? where : undefined;\n }\n\n private buildSort(params: IDataSourceQuery): string[] | undefined {\n if (!params.sort) {\n return undefined;\n }\n return [`${params.sort.field}_${params.sort.direction}`];\n }\n}\n"],"names":["FolderAwareDataSource","config","Set","Object","locationField","localFilters","item","value","folderId","location","itemFolderId","QueryMatcher","makeObservable","observable","computed","action","_data","params","result","runInAction","String","undefined","_where","_filters","where","filters","isRoot","hasAdvancedFilters","k","isSearching","descendants","folderIds","f","key"],"mappings":";;AA8BO,MAAeA;IASlB,YAAYC,MAAyC,CAAE;aAR/C,KAAK,GAAoB;YAAE,QAAQ;YAAM,cAAc;YAAO,YAAY;QAAE;aAC5E,QAAQ,GAAG;QAQf,IAAI,CAAC,cAAc,GAAGA,OAAO,aAAa,IAAI;QAC9C,IAAI,CAAC,sBAAsB,GAAG,IAAIC,IAAI;eAC9BD,OAAO,qBAAqB,IAAI,EAAE;eACnCE,OAAO,IAAI,CAACF,OAAO,YAAY,IAAI,CAAC;SAC1C;QACD,IAAI,CAAC,qBAAqB,GAAGA,OAAO,oBAAoB;QACxD,IAAI,CAAC,SAAS,GAAGA,OAAO,QAAQ;QAEhC,MAAMG,gBAAgB,IAAI,CAAC,cAAc;QACzC,MAAMC,eAAwE;YAC1E,UAAU,CAACC,MAAMC;gBACb,MAAMC,WAAWD;gBACjB,MAAME,WAAYH,IAA4B,CAACF,cAAc;gBAC7D,MAAMM,eAAeD,UAAU;gBAC/B,IAAID,YAAYA,AAAa,WAAbA,UACZ,OAAOE,iBAAiBF;gBAE5B,OAAO,CAACE,gBAAgBA,AAAiB,WAAjBA;YAC5B;YACA,GAAGT,OAAO,YAAY;QAC1B;QAEA,IAAI,CAAC,YAAY,GAAG,IAAIU,aAAmB;YACvC,UAAUV,OAAO,QAAQ;YACzBI;YACA,cAAcJ,OAAO,YAAY;QACrC;QAEAW,eAAkE,IAAI,EAAE;YACpE,OAAOC;YACP,UAAUA;YACV,MAAMC;YACN,SAASA;YACT,OAAOC;YACP,UAAUA;QACd;IACJ;IAMU,cAAcC,KAAa,EAAQ,CAAC;IAEpC,iBAAiBA,KAAa,EAAQ,CAAC;IAEjD,IAAI,OAAwB;QACxB,OAAO,IAAI,CAAC,KAAK;IACrB;IAEA,IAAI,UAAmB;QACnB,OAAO,IAAI,CAAC,QAAQ;IACxB;IAEA,MAAM,MAAMC,MAAwB,EAAiB;QACjD,IAAI,CAAC,QAAQ,GAAG;QAEhB,IAAI;YACA,MAAMC,SAAS,MAAM,IAAI,CAAC,KAAK,CAAC;gBAC5B,OAAO,IAAI,CAAC,UAAU,CAACD;gBACvB,MAAM,IAAI,CAAC,SAAS,CAACA;gBACrB,QAAQA,OAAO,MAAM;gBACrB,OAAOA,OAAO,KAAK;gBACnB,OAAOA,OAAO,MAAM;YACxB;YAEAE,YAAY;gBACR,IAAI,CAAC,aAAa,CAACD,OAAO,IAAI;gBAC9B,IAAI,CAAC,YAAY,CAAC,eAAe,CAC7BD,QACAC,OAAO,IAAI,CAAC,GAAG,CAACZ,CAAAA,OAAQc,OAAOd,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IACnDY,OAAO,IAAI,CAAC,YAAY;gBAE5B,IAAI,CAAC,KAAK,GAAG;oBACT,QAAQA,OAAO,IAAI,CAAC,MAAM;oBAC1B,cAAcA,OAAO,IAAI,CAAC,YAAY;oBACtC,YAAYA,OAAO,IAAI,CAAC,UAAU;gBACtC;YACJ;QACJ,SAAU;YACNC,YAAY;gBACR,IAAI,CAAC,QAAQ,GAAG;YACpB;QACJ;IACJ;IAEA,MAAM,SAASF,MAAwB,EAAiB;QACpD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,QAAQ,EACzC;QAGJ,IAAI,CAAC,QAAQ,GAAG;QAEhB,IAAI;YACA,MAAMC,SAAS,MAAM,IAAI,CAAC,KAAK,CAAC;gBAC5B,OAAO,IAAI,CAAC,UAAU,CAACD;gBACvB,MAAM,IAAI,CAAC,SAAS,CAACA;gBACrB,QAAQA,OAAO,MAAM;gBACrB,OAAOA,OAAO,KAAK;gBACnB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,IAAII;YAChC;YAEAF,YAAY;gBACR,IAAI,CAAC,gBAAgB,CAACD,OAAO,IAAI;gBACjC,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAC9BA,OAAO,IAAI,CAAC,GAAG,CAACZ,CAAAA,OAAQc,OAAOd,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IACnDY,OAAO,IAAI,CAAC,YAAY;gBAE5B,IAAI,CAAC,KAAK,GAAG;oBACT,QAAQA,OAAO,IAAI,CAAC,MAAM;oBAC1B,cAAcA,OAAO,IAAI,CAAC,YAAY;oBACtC,YAAYA,OAAO,IAAI,CAAC,UAAU;gBACtC;YACJ;QACJ,SAAU;YACNC,YAAY;gBACR,IAAI,CAAC,QAAQ,GAAG;YACpB;QACJ;IACJ;IAEU,eACNG,MAA+B,EAC/BC,QAAiC,EAC7B,CAAC;IAEC,oBAAoBA,QAAiC,EAAW;QACtE,OAAO;IACX;IAEQ,WAAWN,MAAwB,EAAuC;QAC9E,MAAMO,QAAiC,CAAC;QACxC,MAAMC,UAAUR,OAAO,OAAO,IAAI,CAAC;QACnC,MAAMT,WAAYiB,QAAQ,QAAQ,IAA2B;QAC7D,MAAMC,SAASlB,AAAa,WAAbA;QACf,MAAMmB,qBAAqBxB,OAAO,IAAI,CAACsB,SAAS,IAAI,CAChDG,CAAAA,IAAKA,AAAM,eAANA,KAAoB,CAAC,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAACA;QAE9D,MAAMC,cACF,CAAC,CAACZ,OAAO,MAAM,IAAIU,sBAAsB,IAAI,CAAC,mBAAmB,CAACF;QAEtE,IAAII,eAAeH;aAEZ,IAAIG,eAAe,IAAI,CAAC,qBAAqB,EAAE;YAClD,MAAMC,cAAc,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAACtB;YACvD,MAAMuB,YAAYD,YAAY,GAAG,CAACE,CAAAA,IAAKA,EAAE,EAAE;YAC3CR,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG;gBAAE,aAAaO;YAAU;QAC1D,OACIP,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG;YAAEhB;QAAS;QAG5C,KAAK,MAAM,CAACyB,KAAK1B,MAAM,IAAIJ,OAAO,OAAO,CAACsB,SACtC,IAAIQ,AAAQ,eAARA,KAGJT,KAAK,CAACS,IAAI,GAAG1B;QAGjB,IAAI,CAAC,cAAc,CAACiB,OAAOC;QAE3B,OAAOtB,OAAO,IAAI,CAACqB,OAAO,MAAM,GAAG,IAAIA,QAAQH;IACnD;IAEQ,UAAUJ,MAAwB,EAAwB;QAC9D,IAAI,CAACA,OAAO,IAAI,EACZ;QAEJ,OAAO;YAAC,GAAGA,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAEA,OAAO,IAAI,CAAC,SAAS,EAAE;SAAC;IAC5D;AACJ"}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { IDataSourceQuery } from "./abstractions.js";
|
|
2
2
|
type ItemPredicate<TRow> = (item: TRow, value: unknown) => boolean;
|
|
3
|
+
type SortValueGetter<TRow> = (item: TRow, field: string) => unknown;
|
|
3
4
|
export interface QueryMatcherConfig<TRow> {
|
|
4
5
|
keyField: keyof TRow & string;
|
|
5
6
|
localFilters: Record<string, ItemPredicate<TRow>>;
|
|
7
|
+
/**
|
|
8
|
+
* Reads the value of a sort field from an item. Defaults to a dot-path lookup
|
|
9
|
+
* (e.g. `savedOn`, `values.title`). Override when the query's sort field name
|
|
10
|
+
* does not match the item's shape.
|
|
11
|
+
*/
|
|
12
|
+
getSortValue?: SortValueGetter<TRow>;
|
|
6
13
|
}
|
|
7
14
|
/**
|
|
8
15
|
* Decides which cached items belong in the current view.
|
|
@@ -34,10 +41,11 @@ export interface QueryMatcherConfig<TRow> {
|
|
|
34
41
|
* });
|
|
35
42
|
*
|
|
36
43
|
* // In DataSource.query(), after fetching:
|
|
37
|
-
* matcher.updateFromQuery(params, result.data.map(item => item.id));
|
|
44
|
+
* matcher.updateFromQuery(params, result.data.map(item => item.id), result.meta.hasMoreItems);
|
|
38
45
|
*
|
|
39
|
-
* // In DataSource.rows getter
|
|
40
|
-
*
|
|
46
|
+
* // In DataSource.rows getter (filters, sorts by the applied sort, and cuts off at the
|
|
47
|
+
* // last loaded item):
|
|
48
|
+
* return matcher.select(cache.getItems());
|
|
41
49
|
* ```
|
|
42
50
|
*/
|
|
43
51
|
export declare class QueryMatcher<TRow> {
|
|
@@ -45,12 +53,27 @@ export declare class QueryMatcher<TRow> {
|
|
|
45
53
|
private _queryResultKeys;
|
|
46
54
|
private _matcher;
|
|
47
55
|
private localFilterKeys;
|
|
56
|
+
private getSortValue;
|
|
57
|
+
private _view;
|
|
48
58
|
constructor(config: QueryMatcherConfig<TRow>);
|
|
49
59
|
get matcher(): (item: TRow) => boolean;
|
|
50
60
|
filter(items: TRow[]): TRow[];
|
|
61
|
+
/**
|
|
62
|
+
* Returns the items that belong in the current view, in the view's order.
|
|
63
|
+
*
|
|
64
|
+
* The cache is shared by views with different sorts, so its order means nothing to
|
|
65
|
+
* this view. Instead, matching items are sorted by the applied sort (ties keep the
|
|
66
|
+
* server order), and the result is cut off after the last item the server returned
|
|
67
|
+
* for this view. That keeps locally created or updated items in their sort position,
|
|
68
|
+
* and hides cached items that sort past the loaded pages until pagination reaches them.
|
|
69
|
+
*
|
|
70
|
+
* While searching, the server's (relevance) order is kept as is.
|
|
71
|
+
*/
|
|
72
|
+
select(items: TRow[]): TRow[];
|
|
73
|
+
private getKey;
|
|
51
74
|
hasServerSideFilters(params: IDataSourceQuery): boolean;
|
|
52
|
-
updateFromQuery(params: IDataSourceQuery, resultKeys: string[]): void;
|
|
53
|
-
appendResultKeys(keys: string[]): void;
|
|
75
|
+
updateFromQuery(params: IDataSourceQuery, resultKeys: string[], hasMore?: boolean): void;
|
|
76
|
+
appendResultKeys(keys: string[], hasMore?: boolean): void;
|
|
54
77
|
private buildMatcher;
|
|
55
78
|
}
|
|
56
79
|
export {};
|
|
@@ -4,9 +4,12 @@ class QueryMatcher {
|
|
|
4
4
|
this.config = config;
|
|
5
5
|
this._queryResultKeys = null;
|
|
6
6
|
this._matcher = ()=>true;
|
|
7
|
+
this._view = null;
|
|
7
8
|
this.localFilterKeys = new Set(Object.keys(config.localFilters));
|
|
9
|
+
this.getSortValue = config.getSortValue ?? getValueByPath;
|
|
8
10
|
makeObservable(this, {
|
|
9
11
|
_matcher: observable.ref,
|
|
12
|
+
_view: observable.ref,
|
|
10
13
|
matcher: computed,
|
|
11
14
|
updateFromQuery: action,
|
|
12
15
|
appendResultKeys: action
|
|
@@ -18,18 +21,65 @@ class QueryMatcher {
|
|
|
18
21
|
filter(items) {
|
|
19
22
|
return items.filter(this._matcher);
|
|
20
23
|
}
|
|
24
|
+
select(items) {
|
|
25
|
+
const matched = this.filter(items);
|
|
26
|
+
const view = this._view;
|
|
27
|
+
if (!view) return matched;
|
|
28
|
+
const serverIndex = new Map(view.resultKeys.map((key, index)=>[
|
|
29
|
+
key,
|
|
30
|
+
index
|
|
31
|
+
]));
|
|
32
|
+
const indexOf = (item)=>serverIndex.get(this.getKey(item)) ?? 1 / 0;
|
|
33
|
+
const sort = view.searching ? void 0 : view.sort;
|
|
34
|
+
const sorted = [
|
|
35
|
+
...matched
|
|
36
|
+
].sort((a, b)=>{
|
|
37
|
+
if (sort) {
|
|
38
|
+
const result = compareValues(this.getSortValue(a, sort.field), this.getSortValue(b, sort.field));
|
|
39
|
+
if (0 !== result) return "ASC" === sort.direction ? result : -result;
|
|
40
|
+
}
|
|
41
|
+
const left = indexOf(a);
|
|
42
|
+
const right = indexOf(b);
|
|
43
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
44
|
+
});
|
|
45
|
+
if (!view.hasMore) return sorted;
|
|
46
|
+
let lastLoaded = -1;
|
|
47
|
+
sorted.forEach((item, index)=>{
|
|
48
|
+
if (serverIndex.has(this.getKey(item))) lastLoaded = index;
|
|
49
|
+
});
|
|
50
|
+
return lastLoaded < 0 ? sorted : sorted.slice(0, lastLoaded + 1);
|
|
51
|
+
}
|
|
52
|
+
getKey(item) {
|
|
53
|
+
return String(item[this.config.keyField]);
|
|
54
|
+
}
|
|
21
55
|
hasServerSideFilters(params) {
|
|
22
56
|
if (params.search) return true;
|
|
23
57
|
const filters = params.filters ?? {};
|
|
24
58
|
return Object.keys(filters).some((key)=>!this.localFilterKeys.has(key));
|
|
25
59
|
}
|
|
26
|
-
updateFromQuery(params, resultKeys) {
|
|
60
|
+
updateFromQuery(params, resultKeys, hasMore = false) {
|
|
27
61
|
if (this.hasServerSideFilters(params)) this._queryResultKeys = new Set(resultKeys);
|
|
28
62
|
else this._queryResultKeys = null;
|
|
29
63
|
this._matcher = this.buildMatcher(params);
|
|
64
|
+
this._view = {
|
|
65
|
+
resultKeys: [
|
|
66
|
+
...resultKeys
|
|
67
|
+
],
|
|
68
|
+
sort: params.sort,
|
|
69
|
+
searching: !!params.search,
|
|
70
|
+
hasMore
|
|
71
|
+
};
|
|
30
72
|
}
|
|
31
|
-
appendResultKeys(keys) {
|
|
73
|
+
appendResultKeys(keys, hasMore = false) {
|
|
32
74
|
if (this._queryResultKeys) for (const key of keys)this._queryResultKeys.add(key);
|
|
75
|
+
if (this._view) this._view = {
|
|
76
|
+
...this._view,
|
|
77
|
+
resultKeys: [
|
|
78
|
+
...this._view.resultKeys,
|
|
79
|
+
...keys
|
|
80
|
+
],
|
|
81
|
+
hasMore
|
|
82
|
+
};
|
|
33
83
|
}
|
|
34
84
|
buildMatcher(params) {
|
|
35
85
|
const filters = params.filters ?? {};
|
|
@@ -54,6 +104,26 @@ class QueryMatcher {
|
|
|
54
104
|
};
|
|
55
105
|
}
|
|
56
106
|
}
|
|
107
|
+
function getValueByPath(item, path) {
|
|
108
|
+
let current = item;
|
|
109
|
+
for (const part of path.split(".")){
|
|
110
|
+
if (null == current || "object" != typeof current) return;
|
|
111
|
+
current = current[part];
|
|
112
|
+
}
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
function compareValues(a, b) {
|
|
116
|
+
const aEmpty = null == a;
|
|
117
|
+
const bEmpty = null == b;
|
|
118
|
+
if (aEmpty || bEmpty) return aEmpty === bEmpty ? 0 : aEmpty ? 1 : -1;
|
|
119
|
+
const left = a instanceof Date ? a.getTime() : a;
|
|
120
|
+
const right = b instanceof Date ? b.getTime() : b;
|
|
121
|
+
if ("number" == typeof left && "number" == typeof right) return left - right;
|
|
122
|
+
if ("boolean" == typeof left && "boolean" == typeof right) return Number(left) - Number(right);
|
|
123
|
+
const leftString = String(left);
|
|
124
|
+
const rightString = String(right);
|
|
125
|
+
return leftString < rightString ? -1 : leftString > rightString ? 1 : 0;
|
|
126
|
+
}
|
|
57
127
|
export { QueryMatcher };
|
|
58
128
|
|
|
59
129
|
//# sourceMappingURL=QueryMatcher.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presentation/listPresenter/QueryMatcher.js","sources":["../../../src/presentation/listPresenter/QueryMatcher.ts"],"sourcesContent":["import { makeObservable, observable, computed, action } from \"mobx\";\nimport type { IDataSourceQuery } from \"./abstractions.js\";\n\ntype ItemPredicate<TRow> = (item: TRow, value: unknown) => boolean;\n\nexport interface QueryMatcherConfig<TRow> {\n keyField: keyof TRow & string;\n localFilters: Record<string, ItemPredicate<TRow>>;\n}\n\n/**\n * Decides which cached items belong in the current view.\n *\n * A DataSource backed by a shared cache (e.g., per-model entry cache) needs to\n * filter cached items to only those matching the active query. QueryMatcher does\n * this by splitting filters into two categories:\n *\n * - **Local filters** (e.g., folderId, status) — evaluated client-side against\n * each item using predicates provided via `localFilters` config. Used when\n * browsing without search or server-side filters.\n *\n * - **Server-side filters** (search, advanced search, any filter key not in\n * `localFilters`) — cannot be evaluated locally. Instead, QueryMatcher tracks\n * the set of item keys returned by the last server query (`_queryResultKeys`)\n * and restricts the view to those items.\n *\n * The `matcher` getter is MobX-computed, so any DataSource `rows` getter that\n * reads it will recompute when the matcher changes after a query.\n *\n * Usage:\n * ```ts\n * const matcher = new QueryMatcher<MyItem>({\n * keyField: \"id\",\n * localFilters: {\n * folderId: (item, value) => item.folderId === value,\n * status: (item, value) => item.status === value,\n * }\n * });\n *\n * // In DataSource.query(), after fetching:\n * matcher.updateFromQuery(params, result.data.map(item => item.id));\n *\n * // In DataSource.rows getter:\n * return cache.getItems().filter(matcher.matcher);\n * ```\n */\nexport class QueryMatcher<TRow> {\n private _queryResultKeys: Set<string> | null = null;\n private _matcher: (item: TRow) => boolean = () => true;\n private localFilterKeys: Set<string>;\n\n constructor(private config: QueryMatcherConfig<TRow>) {\n this.localFilterKeys = new Set(Object.keys(config.localFilters));\n\n makeObservable<QueryMatcher<TRow>, \"_matcher\">(this, {\n _matcher: observable.ref,\n matcher: computed,\n updateFromQuery: action,\n appendResultKeys: action\n });\n }\n\n get matcher(): (item: TRow) => boolean {\n return this._matcher;\n }\n\n filter(items: TRow[]): TRow[] {\n return items.filter(this._matcher);\n }\n\n hasServerSideFilters(params: IDataSourceQuery): boolean {\n if (params.search) {\n return true;\n }\n const filters = params.filters ?? {};\n return Object.keys(filters).some(key => !this.localFilterKeys.has(key));\n }\n\n updateFromQuery(params: IDataSourceQuery, resultKeys: string[]): void {\n if (this.hasServerSideFilters(params)) {\n this._queryResultKeys = new Set(resultKeys);\n } else {\n this._queryResultKeys = null;\n }\n this._matcher = this.buildMatcher(params);\n }\n\n appendResultKeys(keys: string[]): void {\n if (this._queryResultKeys) {\n for (const key of keys) {\n this._queryResultKeys.add(key);\n }\n }\n }\n\n private buildMatcher(params: IDataSourceQuery): (item: TRow) => boolean {\n const filters = params.filters ?? {};\n const queryResultKeys = this._queryResultKeys;\n const isSearching = !!params.search;\n const { keyField, localFilters } = this.config;\n\n const activeLocalFilters: Array<{ predicate: ItemPredicate<TRow>; value: unknown }> = [];\n for (const [key, predicate] of Object.entries(localFilters)) {\n const value = filters[key];\n if (value !== undefined && value !== null) {\n activeLocalFilters.push({ predicate, value });\n }\n }\n\n return (item: TRow) => {\n if (queryResultKeys) {\n if (!queryResultKeys.has(String(item[keyField]))) {\n return false;\n }\n } else if (!isSearching) {\n for (const { predicate, value } of activeLocalFilters) {\n if (!predicate(item, value)) {\n return false;\n }\n }\n }\n\n return true;\n };\n }\n}\n"],"names":["QueryMatcher","config","Set","Object","makeObservable","observable","computed","action","items","params","filters","key","resultKeys","keys","queryResultKeys","isSearching","keyField","localFilters","activeLocalFilters","predicate","value","item","String"],"mappings":";AA8CO,MAAMA;IAKT,YAAoBC,MAAgC,CAAE;aAAlCA,MAAM,GAANA;aAJZ,gBAAgB,GAAuB;aACvC,QAAQ,GAA4B,IAAM;QAI9C,IAAI,CAAC,eAAe,GAAG,IAAIC,IAAIC,OAAO,IAAI,CAACF,OAAO,YAAY;QAE9DG,eAA+C,IAAI,EAAE;YACjD,UAAUC,WAAW,GAAG;YACxB,SAASC;YACT,iBAAiBC;YACjB,kBAAkBA;QACtB;IACJ;IAEA,IAAI,UAAmC;QACnC,OAAO,IAAI,CAAC,QAAQ;IACxB;IAEA,OAAOC,KAAa,EAAU;QAC1B,OAAOA,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ;IACrC;IAEA,qBAAqBC,MAAwB,EAAW;QACpD,IAAIA,OAAO,MAAM,EACb,OAAO;QAEX,MAAMC,UAAUD,OAAO,OAAO,IAAI,CAAC;QACnC,OAAON,OAAO,IAAI,CAACO,SAAS,IAAI,CAACC,CAAAA,MAAO,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAACA;IACtE;IAEA,gBAAgBF,MAAwB,EAAEG,UAAoB,EAAQ;QAClE,IAAI,IAAI,CAAC,oBAAoB,CAACH,SAC1B,IAAI,CAAC,gBAAgB,GAAG,IAAIP,IAAIU;aAEhC,IAAI,CAAC,gBAAgB,GAAG;QAE5B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,YAAY,CAACH;IACtC;IAEA,iBAAiBI,IAAc,EAAQ;QACnC,IAAI,IAAI,CAAC,gBAAgB,EACrB,KAAK,MAAMF,OAAOE,KACd,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAACF;IAGtC;IAEQ,aAAaF,MAAwB,EAA2B;QACpE,MAAMC,UAAUD,OAAO,OAAO,IAAI,CAAC;QACnC,MAAMK,kBAAkB,IAAI,CAAC,gBAAgB;QAC7C,MAAMC,cAAc,CAAC,CAACN,OAAO,MAAM;QACnC,MAAM,EAAEO,QAAQ,EAAEC,YAAY,EAAE,GAAG,IAAI,CAAC,MAAM;QAE9C,MAAMC,qBAAgF,EAAE;QACxF,KAAK,MAAM,CAACP,KAAKQ,UAAU,IAAIhB,OAAO,OAAO,CAACc,cAAe;YACzD,MAAMG,QAAQV,OAAO,CAACC,IAAI;YAC1B,IAAIS,QAAAA,OACAF,mBAAmB,IAAI,CAAC;gBAAEC;gBAAWC;YAAM;QAEnD;QAEA,OAAO,CAACC;YACJ,IAAIP,iBACA;gBAAA,IAAI,CAACA,gBAAgB,GAAG,CAACQ,OAAOD,IAAI,CAACL,SAAS,IAC1C,OAAO;YACX,OACG,IAAI,CAACD,aACR;gBAAA,KAAK,MAAM,EAAEI,SAAS,EAAEC,KAAK,EAAE,IAAIF,mBAC/B,IAAI,CAACC,UAAUE,MAAMD,QACjB,OAAO;YAEf;YAGJ,OAAO;QACX;IACJ;AACJ"}
|
|
1
|
+
{"version":3,"file":"presentation/listPresenter/QueryMatcher.js","sources":["../../../src/presentation/listPresenter/QueryMatcher.ts"],"sourcesContent":["import { makeObservable, observable, computed, action } from \"mobx\";\nimport type { IDataSourceQuery } from \"./abstractions.js\";\n\ntype ItemPredicate<TRow> = (item: TRow, value: unknown) => boolean;\n\ntype SortValueGetter<TRow> = (item: TRow, field: string) => unknown;\n\nexport interface QueryMatcherConfig<TRow> {\n keyField: keyof TRow & string;\n localFilters: Record<string, ItemPredicate<TRow>>;\n /**\n * Reads the value of a sort field from an item. Defaults to a dot-path lookup\n * (e.g. `savedOn`, `values.title`). Override when the query's sort field name\n * does not match the item's shape.\n */\n getSortValue?: SortValueGetter<TRow>;\n}\n\n/**\n * Decides which cached items belong in the current view.\n *\n * A DataSource backed by a shared cache (e.g., per-model entry cache) needs to\n * filter cached items to only those matching the active query. QueryMatcher does\n * this by splitting filters into two categories:\n *\n * - **Local filters** (e.g., folderId, status) — evaluated client-side against\n * each item using predicates provided via `localFilters` config. Used when\n * browsing without search or server-side filters.\n *\n * - **Server-side filters** (search, advanced search, any filter key not in\n * `localFilters`) — cannot be evaluated locally. Instead, QueryMatcher tracks\n * the set of item keys returned by the last server query (`_queryResultKeys`)\n * and restricts the view to those items.\n *\n * The `matcher` getter is MobX-computed, so any DataSource `rows` getter that\n * reads it will recompute when the matcher changes after a query.\n *\n * Usage:\n * ```ts\n * const matcher = new QueryMatcher<MyItem>({\n * keyField: \"id\",\n * localFilters: {\n * folderId: (item, value) => item.folderId === value,\n * status: (item, value) => item.status === value,\n * }\n * });\n *\n * // In DataSource.query(), after fetching:\n * matcher.updateFromQuery(params, result.data.map(item => item.id), result.meta.hasMoreItems);\n *\n * // In DataSource.rows getter (filters, sorts by the applied sort, and cuts off at the\n * // last loaded item):\n * return matcher.select(cache.getItems());\n * ```\n */\nexport class QueryMatcher<TRow> {\n private _queryResultKeys: Set<string> | null = null;\n private _matcher: (item: TRow) => boolean = () => true;\n private localFilterKeys: Set<string>;\n private getSortValue: SortValueGetter<TRow>;\n\n // Ordering state of the current view: the keys the server returned for the active\n // query (in server order, extended by `appendResultKeys`), the applied sort, and\n // whether the server has more items beyond the loaded ones.\n private _view: {\n resultKeys: string[];\n sort: IDataSourceQuery[\"sort\"];\n searching: boolean;\n hasMore: boolean;\n } | null = null;\n\n constructor(private config: QueryMatcherConfig<TRow>) {\n this.localFilterKeys = new Set(Object.keys(config.localFilters));\n this.getSortValue = config.getSortValue ?? getValueByPath;\n\n makeObservable<QueryMatcher<TRow>, \"_matcher\" | \"_view\">(this, {\n _matcher: observable.ref,\n _view: observable.ref,\n matcher: computed,\n updateFromQuery: action,\n appendResultKeys: action\n });\n }\n\n get matcher(): (item: TRow) => boolean {\n return this._matcher;\n }\n\n filter(items: TRow[]): TRow[] {\n return items.filter(this._matcher);\n }\n\n /**\n * Returns the items that belong in the current view, in the view's order.\n *\n * The cache is shared by views with different sorts, so its order means nothing to\n * this view. Instead, matching items are sorted by the applied sort (ties keep the\n * server order), and the result is cut off after the last item the server returned\n * for this view. That keeps locally created or updated items in their sort position,\n * and hides cached items that sort past the loaded pages until pagination reaches them.\n *\n * While searching, the server's (relevance) order is kept as is.\n */\n select(items: TRow[]): TRow[] {\n const matched = this.filter(items);\n const view = this._view;\n if (!view) {\n return matched;\n }\n\n const serverIndex = new Map(view.resultKeys.map((key, index) => [key, index]));\n // Items the server did not return for this view (cached by other views, or created\n // locally) sort after the server's items among equal sort values.\n const indexOf = (item: TRow) =>\n serverIndex.get(this.getKey(item)) ?? Number.POSITIVE_INFINITY;\n\n const sort = view.searching ? undefined : view.sort;\n const sorted = [...matched].sort((a, b) => {\n if (sort) {\n const result = compareValues(\n this.getSortValue(a, sort.field),\n this.getSortValue(b, sort.field)\n );\n if (result !== 0) {\n return sort.direction === \"ASC\" ? result : -result;\n }\n }\n const left = indexOf(a);\n const right = indexOf(b);\n return left === right ? 0 : left < right ? -1 : 1;\n });\n\n if (!view.hasMore) {\n return sorted;\n }\n\n let lastLoaded = -1;\n sorted.forEach((item, index) => {\n if (serverIndex.has(this.getKey(item))) {\n lastLoaded = index;\n }\n });\n\n return lastLoaded < 0 ? sorted : sorted.slice(0, lastLoaded + 1);\n }\n\n private getKey(item: TRow): string {\n return String(item[this.config.keyField]);\n }\n\n hasServerSideFilters(params: IDataSourceQuery): boolean {\n if (params.search) {\n return true;\n }\n const filters = params.filters ?? {};\n return Object.keys(filters).some(key => !this.localFilterKeys.has(key));\n }\n\n updateFromQuery(params: IDataSourceQuery, resultKeys: string[], hasMore = false): void {\n if (this.hasServerSideFilters(params)) {\n this._queryResultKeys = new Set(resultKeys);\n } else {\n this._queryResultKeys = null;\n }\n this._matcher = this.buildMatcher(params);\n this._view = {\n resultKeys: [...resultKeys],\n sort: params.sort,\n searching: !!params.search,\n hasMore\n };\n }\n\n appendResultKeys(keys: string[], hasMore = false): void {\n if (this._queryResultKeys) {\n for (const key of keys) {\n this._queryResultKeys.add(key);\n }\n }\n if (this._view) {\n this._view = {\n ...this._view,\n resultKeys: [...this._view.resultKeys, ...keys],\n hasMore\n };\n }\n }\n\n private buildMatcher(params: IDataSourceQuery): (item: TRow) => boolean {\n const filters = params.filters ?? {};\n const queryResultKeys = this._queryResultKeys;\n const isSearching = !!params.search;\n const { keyField, localFilters } = this.config;\n\n const activeLocalFilters: Array<{ predicate: ItemPredicate<TRow>; value: unknown }> = [];\n for (const [key, predicate] of Object.entries(localFilters)) {\n const value = filters[key];\n if (value !== undefined && value !== null) {\n activeLocalFilters.push({ predicate, value });\n }\n }\n\n return (item: TRow) => {\n if (queryResultKeys) {\n if (!queryResultKeys.has(String(item[keyField]))) {\n return false;\n }\n } else if (!isSearching) {\n for (const { predicate, value } of activeLocalFilters) {\n if (!predicate(item, value)) {\n return false;\n }\n }\n }\n\n return true;\n };\n }\n}\n\nfunction getValueByPath(item: unknown, path: string): unknown {\n let current: unknown = item;\n for (const part of path.split(\".\")) {\n if (current === null || current === undefined || typeof current !== \"object\") {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\n/**\n * Compares two sort values in ascending order, the same way the DDB and SQL storage sort\n * entries: `null` and `undefined` sort last (so first in descending order), and an empty\n * string is a regular string. Strings are compared by code point, which keeps ISO dates in\n * chronological order.\n */\nfunction compareValues(a: unknown, b: unknown): number {\n const aEmpty = a === null || a === undefined;\n const bEmpty = b === null || b === undefined;\n if (aEmpty || bEmpty) {\n return aEmpty === bEmpty ? 0 : aEmpty ? 1 : -1;\n }\n\n const left = a instanceof Date ? a.getTime() : a;\n const right = b instanceof Date ? b.getTime() : b;\n\n if (typeof left === \"number\" && typeof right === \"number\") {\n return left - right;\n }\n if (typeof left === \"boolean\" && typeof right === \"boolean\") {\n return Number(left) - Number(right);\n }\n\n const leftString = String(left);\n const rightString = String(right);\n return leftString < rightString ? -1 : leftString > rightString ? 1 : 0;\n}\n"],"names":["QueryMatcher","config","Set","Object","getValueByPath","makeObservable","observable","computed","action","items","matched","view","serverIndex","Map","key","index","indexOf","item","Number","sort","undefined","sorted","a","b","result","compareValues","left","right","lastLoaded","String","params","filters","resultKeys","hasMore","keys","queryResultKeys","isSearching","keyField","localFilters","activeLocalFilters","predicate","value","path","current","part","aEmpty","bEmpty","Date","leftString","rightString"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { QueryMatcher } from "./QueryMatcher.js";
|
|
3
|
+
const row = (id, savedOn, folderId = "root")=>({
|
|
4
|
+
id,
|
|
5
|
+
folderId,
|
|
6
|
+
savedOn
|
|
7
|
+
});
|
|
8
|
+
const createMatcher = (config = {})=>new QueryMatcher({
|
|
9
|
+
keyField: "id",
|
|
10
|
+
localFilters: {
|
|
11
|
+
folderId: (item, value)=>item.folderId === value
|
|
12
|
+
},
|
|
13
|
+
...config
|
|
14
|
+
});
|
|
15
|
+
const ids = (rows)=>rows.map((r)=>r.id);
|
|
16
|
+
describe("QueryMatcher.select", ()=>{
|
|
17
|
+
it("should only filter before the first query", ()=>{
|
|
18
|
+
const matcher = createMatcher();
|
|
19
|
+
expect(ids(matcher.select([
|
|
20
|
+
row("b", "2"),
|
|
21
|
+
row("a", "1")
|
|
22
|
+
]))).toEqual([
|
|
23
|
+
"b",
|
|
24
|
+
"a"
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
it("should sort by the applied sort, regardless of cache order", ()=>{
|
|
28
|
+
const matcher = createMatcher();
|
|
29
|
+
matcher.updateFromQuery({
|
|
30
|
+
sort: {
|
|
31
|
+
field: "savedOn",
|
|
32
|
+
direction: "DESC"
|
|
33
|
+
}
|
|
34
|
+
}, [
|
|
35
|
+
"c",
|
|
36
|
+
"b",
|
|
37
|
+
"a"
|
|
38
|
+
]);
|
|
39
|
+
const cached = [
|
|
40
|
+
row("a", "1"),
|
|
41
|
+
row("c", "3"),
|
|
42
|
+
row("b", "2")
|
|
43
|
+
];
|
|
44
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
45
|
+
"c",
|
|
46
|
+
"b",
|
|
47
|
+
"a"
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
it("should apply local filters", ()=>{
|
|
51
|
+
const matcher = createMatcher();
|
|
52
|
+
matcher.updateFromQuery({
|
|
53
|
+
filters: {
|
|
54
|
+
folderId: "root"
|
|
55
|
+
},
|
|
56
|
+
sort: {
|
|
57
|
+
field: "savedOn",
|
|
58
|
+
direction: "ASC"
|
|
59
|
+
}
|
|
60
|
+
}, [
|
|
61
|
+
"a",
|
|
62
|
+
"b"
|
|
63
|
+
]);
|
|
64
|
+
const cached = [
|
|
65
|
+
row("a", "1"),
|
|
66
|
+
row("x", "0", "other"),
|
|
67
|
+
row("b", "2")
|
|
68
|
+
];
|
|
69
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
70
|
+
"a",
|
|
71
|
+
"b"
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
it("should cut off items that sort past the last loaded item while more are available", ()=>{
|
|
75
|
+
const matcher = createMatcher();
|
|
76
|
+
matcher.updateFromQuery({
|
|
77
|
+
sort: {
|
|
78
|
+
field: "savedOn",
|
|
79
|
+
direction: "ASC"
|
|
80
|
+
}
|
|
81
|
+
}, [
|
|
82
|
+
"a",
|
|
83
|
+
"b"
|
|
84
|
+
], true);
|
|
85
|
+
const cached = [
|
|
86
|
+
row("z", "9"),
|
|
87
|
+
row("a", "1"),
|
|
88
|
+
row("b", "2")
|
|
89
|
+
];
|
|
90
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
91
|
+
"a",
|
|
92
|
+
"b"
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
it("should include items past the last loaded item when nothing more is available", ()=>{
|
|
96
|
+
const matcher = createMatcher();
|
|
97
|
+
matcher.updateFromQuery({
|
|
98
|
+
sort: {
|
|
99
|
+
field: "savedOn",
|
|
100
|
+
direction: "ASC"
|
|
101
|
+
}
|
|
102
|
+
}, [
|
|
103
|
+
"a",
|
|
104
|
+
"b"
|
|
105
|
+
], false);
|
|
106
|
+
const cached = [
|
|
107
|
+
row("z", "9"),
|
|
108
|
+
row("a", "1"),
|
|
109
|
+
row("b", "2")
|
|
110
|
+
];
|
|
111
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
112
|
+
"a",
|
|
113
|
+
"b",
|
|
114
|
+
"z"
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
it("should extend the loaded window on appendResultKeys", ()=>{
|
|
118
|
+
const matcher = createMatcher();
|
|
119
|
+
matcher.updateFromQuery({
|
|
120
|
+
sort: {
|
|
121
|
+
field: "savedOn",
|
|
122
|
+
direction: "ASC"
|
|
123
|
+
}
|
|
124
|
+
}, [
|
|
125
|
+
"a"
|
|
126
|
+
], true);
|
|
127
|
+
matcher.appendResultKeys([
|
|
128
|
+
"b"
|
|
129
|
+
], true);
|
|
130
|
+
const cached = [
|
|
131
|
+
row("c", "3"),
|
|
132
|
+
row("b", "2"),
|
|
133
|
+
row("a", "1")
|
|
134
|
+
];
|
|
135
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
136
|
+
"a",
|
|
137
|
+
"b"
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
it("should keep the server order for items with equal sort values", ()=>{
|
|
141
|
+
const matcher = createMatcher();
|
|
142
|
+
matcher.updateFromQuery({
|
|
143
|
+
sort: {
|
|
144
|
+
field: "savedOn",
|
|
145
|
+
direction: "DESC"
|
|
146
|
+
}
|
|
147
|
+
}, [
|
|
148
|
+
"b",
|
|
149
|
+
"a",
|
|
150
|
+
"c"
|
|
151
|
+
]);
|
|
152
|
+
const cached = [
|
|
153
|
+
row("a", "1"),
|
|
154
|
+
row("c", "1"),
|
|
155
|
+
row("b", "1")
|
|
156
|
+
];
|
|
157
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
158
|
+
"b",
|
|
159
|
+
"a",
|
|
160
|
+
"c"
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
it("should sort empty values last in ascending order", ()=>{
|
|
164
|
+
const matcher = createMatcher();
|
|
165
|
+
matcher.updateFromQuery({
|
|
166
|
+
sort: {
|
|
167
|
+
field: "savedOn",
|
|
168
|
+
direction: "ASC"
|
|
169
|
+
}
|
|
170
|
+
}, [
|
|
171
|
+
"a",
|
|
172
|
+
"b"
|
|
173
|
+
]);
|
|
174
|
+
const cached = [
|
|
175
|
+
row("n"),
|
|
176
|
+
row("b", "2"),
|
|
177
|
+
row("u"),
|
|
178
|
+
row("a", "1")
|
|
179
|
+
];
|
|
180
|
+
cached[0].savedOn = null;
|
|
181
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
182
|
+
"a",
|
|
183
|
+
"b",
|
|
184
|
+
"n",
|
|
185
|
+
"u"
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
it("should sort empty values first in descending order", ()=>{
|
|
189
|
+
const matcher = createMatcher();
|
|
190
|
+
matcher.updateFromQuery({
|
|
191
|
+
sort: {
|
|
192
|
+
field: "savedOn",
|
|
193
|
+
direction: "DESC"
|
|
194
|
+
}
|
|
195
|
+
}, [
|
|
196
|
+
"b",
|
|
197
|
+
"a"
|
|
198
|
+
]);
|
|
199
|
+
expect(ids(matcher.select([
|
|
200
|
+
row("a", "1"),
|
|
201
|
+
row("e"),
|
|
202
|
+
row("b", "2")
|
|
203
|
+
]))).toEqual([
|
|
204
|
+
"e",
|
|
205
|
+
"b",
|
|
206
|
+
"a"
|
|
207
|
+
]);
|
|
208
|
+
});
|
|
209
|
+
it("should sort an empty string as a regular string", ()=>{
|
|
210
|
+
const matcher = createMatcher();
|
|
211
|
+
matcher.updateFromQuery({
|
|
212
|
+
sort: {
|
|
213
|
+
field: "savedOn",
|
|
214
|
+
direction: "ASC"
|
|
215
|
+
}
|
|
216
|
+
}, [
|
|
217
|
+
"a",
|
|
218
|
+
"b"
|
|
219
|
+
]);
|
|
220
|
+
expect(ids(matcher.select([
|
|
221
|
+
row("b", "1"),
|
|
222
|
+
row("u"),
|
|
223
|
+
row("a", "")
|
|
224
|
+
]))).toEqual([
|
|
225
|
+
"a",
|
|
226
|
+
"b",
|
|
227
|
+
"u"
|
|
228
|
+
]);
|
|
229
|
+
});
|
|
230
|
+
it("should keep the server order while searching", ()=>{
|
|
231
|
+
const matcher = createMatcher();
|
|
232
|
+
matcher.updateFromQuery({
|
|
233
|
+
search: "hello",
|
|
234
|
+
sort: {
|
|
235
|
+
field: "savedOn",
|
|
236
|
+
direction: "ASC"
|
|
237
|
+
}
|
|
238
|
+
}, [
|
|
239
|
+
"c",
|
|
240
|
+
"a",
|
|
241
|
+
"b"
|
|
242
|
+
]);
|
|
243
|
+
const cached = [
|
|
244
|
+
row("a", "1"),
|
|
245
|
+
row("b", "2"),
|
|
246
|
+
row("c", "3"),
|
|
247
|
+
row("x", "0")
|
|
248
|
+
];
|
|
249
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
250
|
+
"c",
|
|
251
|
+
"a",
|
|
252
|
+
"b"
|
|
253
|
+
]);
|
|
254
|
+
});
|
|
255
|
+
it("should read nested sort fields by path", ()=>{
|
|
256
|
+
const matcher = createMatcher();
|
|
257
|
+
matcher.updateFromQuery({
|
|
258
|
+
sort: {
|
|
259
|
+
field: "values.title",
|
|
260
|
+
direction: "ASC"
|
|
261
|
+
}
|
|
262
|
+
}, [
|
|
263
|
+
"a",
|
|
264
|
+
"b"
|
|
265
|
+
]);
|
|
266
|
+
const cached = [
|
|
267
|
+
{
|
|
268
|
+
id: "b",
|
|
269
|
+
folderId: "root",
|
|
270
|
+
values: {
|
|
271
|
+
title: "Beta"
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "a",
|
|
276
|
+
folderId: "root",
|
|
277
|
+
values: {
|
|
278
|
+
title: "Alpha"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
];
|
|
282
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
283
|
+
"a",
|
|
284
|
+
"b"
|
|
285
|
+
]);
|
|
286
|
+
});
|
|
287
|
+
it("should use a custom sort value getter", ()=>{
|
|
288
|
+
const matcher = createMatcher({
|
|
289
|
+
getSortValue: (item, field)=>"title" === field ? item.values?.title : void 0
|
|
290
|
+
});
|
|
291
|
+
matcher.updateFromQuery({
|
|
292
|
+
sort: {
|
|
293
|
+
field: "title",
|
|
294
|
+
direction: "DESC"
|
|
295
|
+
}
|
|
296
|
+
}, [
|
|
297
|
+
"a",
|
|
298
|
+
"b"
|
|
299
|
+
]);
|
|
300
|
+
const cached = [
|
|
301
|
+
{
|
|
302
|
+
id: "a",
|
|
303
|
+
folderId: "root",
|
|
304
|
+
values: {
|
|
305
|
+
title: "Alpha"
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: "b",
|
|
310
|
+
folderId: "root",
|
|
311
|
+
values: {
|
|
312
|
+
title: "Beta"
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
];
|
|
316
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
317
|
+
"b",
|
|
318
|
+
"a"
|
|
319
|
+
]);
|
|
320
|
+
});
|
|
321
|
+
it("should put items the server did not return after the server's items among equal values", ()=>{
|
|
322
|
+
const matcher = createMatcher();
|
|
323
|
+
matcher.updateFromQuery({
|
|
324
|
+
sort: {
|
|
325
|
+
field: "savedOn",
|
|
326
|
+
direction: "DESC"
|
|
327
|
+
}
|
|
328
|
+
}, [
|
|
329
|
+
"b",
|
|
330
|
+
"a"
|
|
331
|
+
], true);
|
|
332
|
+
const cached = [
|
|
333
|
+
row("x", "1"),
|
|
334
|
+
row("a", "1"),
|
|
335
|
+
row("y", "1"),
|
|
336
|
+
row("b", "1")
|
|
337
|
+
];
|
|
338
|
+
expect(ids(matcher.select(cached))).toEqual([
|
|
339
|
+
"b",
|
|
340
|
+
"a"
|
|
341
|
+
]);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
//# sourceMappingURL=QueryMatcher.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presentation/listPresenter/QueryMatcher.test.js","sources":["../../../src/presentation/listPresenter/QueryMatcher.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { QueryMatcher } from \"./QueryMatcher.js\";\n\ninterface Row {\n id: string;\n folderId: string;\n savedOn?: string;\n values?: { title?: string };\n}\n\nconst row = (id: string, savedOn?: string, folderId = \"root\"): Row => ({ id, folderId, savedOn });\n\nconst createMatcher = (config: Partial<ConstructorParameters<typeof QueryMatcher<Row>>[0]> = {}) =>\n new QueryMatcher<Row>({\n keyField: \"id\",\n localFilters: {\n folderId: (item, value) => item.folderId === value\n },\n ...config\n });\n\nconst ids = (rows: Row[]) => rows.map(r => r.id);\n\ndescribe(\"QueryMatcher.select\", () => {\n it(\"should only filter before the first query\", () => {\n const matcher = createMatcher();\n\n expect(ids(matcher.select([row(\"b\", \"2\"), row(\"a\", \"1\")]))).toEqual([\"b\", \"a\"]);\n });\n\n it(\"should sort by the applied sort, regardless of cache order\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery({ sort: { field: \"savedOn\", direction: \"DESC\" } }, [\"c\", \"b\", \"a\"]);\n\n const cached = [row(\"a\", \"1\"), row(\"c\", \"3\"), row(\"b\", \"2\")];\n\n expect(ids(matcher.select(cached))).toEqual([\"c\", \"b\", \"a\"]);\n });\n\n it(\"should apply local filters\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery(\n { filters: { folderId: \"root\" }, sort: { field: \"savedOn\", direction: \"ASC\" } },\n [\"a\", \"b\"]\n );\n\n const cached = [row(\"a\", \"1\"), row(\"x\", \"0\", \"other\"), row(\"b\", \"2\")];\n\n expect(ids(matcher.select(cached))).toEqual([\"a\", \"b\"]);\n });\n\n it(\"should cut off items that sort past the last loaded item while more are available\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery({ sort: { field: \"savedOn\", direction: \"ASC\" } }, [\"a\", \"b\"], true);\n\n // \"z\" was cached by another view and sorts after the loaded window.\n const cached = [row(\"z\", \"9\"), row(\"a\", \"1\"), row(\"b\", \"2\")];\n\n expect(ids(matcher.select(cached))).toEqual([\"a\", \"b\"]);\n });\n\n it(\"should include items past the last loaded item when nothing more is available\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery(\n { sort: { field: \"savedOn\", direction: \"ASC\" } },\n [\"a\", \"b\"],\n false\n );\n\n const cached = [row(\"z\", \"9\"), row(\"a\", \"1\"), row(\"b\", \"2\")];\n\n expect(ids(matcher.select(cached))).toEqual([\"a\", \"b\", \"z\"]);\n });\n\n it(\"should extend the loaded window on appendResultKeys\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery({ sort: { field: \"savedOn\", direction: \"ASC\" } }, [\"a\"], true);\n matcher.appendResultKeys([\"b\"], true);\n\n const cached = [row(\"c\", \"3\"), row(\"b\", \"2\"), row(\"a\", \"1\")];\n\n expect(ids(matcher.select(cached))).toEqual([\"a\", \"b\"]);\n });\n\n it(\"should keep the server order for items with equal sort values\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery({ sort: { field: \"savedOn\", direction: \"DESC\" } }, [\"b\", \"a\", \"c\"]);\n\n const cached = [row(\"a\", \"1\"), row(\"c\", \"1\"), row(\"b\", \"1\")];\n\n expect(ids(matcher.select(cached))).toEqual([\"b\", \"a\", \"c\"]);\n });\n\n // Empty values are ordered the same way as the DDB and SQL storage sort them.\n it(\"should sort empty values last in ascending order\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery({ sort: { field: \"savedOn\", direction: \"ASC\" } }, [\"a\", \"b\"]);\n\n const cached = [row(\"n\"), row(\"b\", \"2\"), row(\"u\"), row(\"a\", \"1\")];\n cached[0].savedOn = null as unknown as string;\n\n expect(ids(matcher.select(cached))).toEqual([\"a\", \"b\", \"n\", \"u\"]);\n });\n\n it(\"should sort empty values first in descending order\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery({ sort: { field: \"savedOn\", direction: \"DESC\" } }, [\"b\", \"a\"]);\n\n expect(ids(matcher.select([row(\"a\", \"1\"), row(\"e\"), row(\"b\", \"2\")]))).toEqual([\n \"e\",\n \"b\",\n \"a\"\n ]);\n });\n\n it(\"should sort an empty string as a regular string\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery({ sort: { field: \"savedOn\", direction: \"ASC\" } }, [\"a\", \"b\"]);\n\n expect(ids(matcher.select([row(\"b\", \"1\"), row(\"u\"), row(\"a\", \"\")]))).toEqual([\n \"a\",\n \"b\",\n \"u\"\n ]);\n });\n\n it(\"should keep the server order while searching\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery({ search: \"hello\", sort: { field: \"savedOn\", direction: \"ASC\" } }, [\n \"c\",\n \"a\",\n \"b\"\n ]);\n\n const cached = [row(\"a\", \"1\"), row(\"b\", \"2\"), row(\"c\", \"3\"), row(\"x\", \"0\")];\n\n expect(ids(matcher.select(cached))).toEqual([\"c\", \"a\", \"b\"]);\n });\n\n it(\"should read nested sort fields by path\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery({ sort: { field: \"values.title\", direction: \"ASC\" } }, [\"a\", \"b\"]);\n\n const cached: Row[] = [\n { id: \"b\", folderId: \"root\", values: { title: \"Beta\" } },\n { id: \"a\", folderId: \"root\", values: { title: \"Alpha\" } }\n ];\n\n expect(ids(matcher.select(cached))).toEqual([\"a\", \"b\"]);\n });\n\n it(\"should use a custom sort value getter\", () => {\n const matcher = createMatcher({\n getSortValue: (item, field) => (field === \"title\" ? item.values?.title : undefined)\n });\n matcher.updateFromQuery({ sort: { field: \"title\", direction: \"DESC\" } }, [\"a\", \"b\"]);\n\n const cached: Row[] = [\n { id: \"a\", folderId: \"root\", values: { title: \"Alpha\" } },\n { id: \"b\", folderId: \"root\", values: { title: \"Beta\" } }\n ];\n\n expect(ids(matcher.select(cached))).toEqual([\"b\", \"a\"]);\n });\n\n it(\"should put items the server did not return after the server's items among equal values\", () => {\n const matcher = createMatcher();\n matcher.updateFromQuery(\n { sort: { field: \"savedOn\", direction: \"DESC\" } },\n [\"b\", \"a\"],\n true\n );\n\n // \"x\" and \"y\" were cached by another view and tie with the loaded items.\n const cached = [row(\"x\", \"1\"), row(\"a\", \"1\"), row(\"y\", \"1\"), row(\"b\", \"1\")];\n\n // Server items come first, in server order, and the tied leftovers are cut off\n // together with everything else past the last loaded item.\n expect(ids(matcher.select(cached))).toEqual([\"b\", \"a\"]);\n });\n});\n"],"names":["row","id","savedOn","folderId","createMatcher","config","QueryMatcher","item","value","ids","rows","r","describe","it","matcher","expect","cached","field","undefined"],"mappings":"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"}
|