@open-mercato/core 0.6.8-develop.7029.1.a1bb3363af → 0.6.8-develop.7031.1.005201cd70
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/dist/modules/api_keys/backend/api-keys/page.js +3 -1
- package/dist/modules/api_keys/backend/api-keys/page.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentLibrary.js +2 -0
- package/dist/modules/attachments/components/AttachmentLibrary.js.map +2 -2
- package/dist/modules/audit_logs/backend/audit-logs/page.js +6 -0
- package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
- package/dist/modules/auth/backend/roles/page.js +3 -1
- package/dist/modules/auth/backend/roles/page.js.map +2 -2
- package/dist/modules/auth/backend/users/page.js +2 -1
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/business_rules/backend/logs/page.js +3 -1
- package/dist/modules/business_rules/backend/logs/page.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/page.js +3 -1
- package/dist/modules/business_rules/backend/rules/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/page.js +3 -1
- package/dist/modules/business_rules/backend/sets/page.js.map +2 -2
- package/dist/modules/catalog/ai-agents.js +1 -1
- package/dist/modules/catalog/ai-agents.js.map +1 -1
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +2 -2
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +5 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +2 -2
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js +2 -0
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductsDataTable.js +7 -1
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +2 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +3 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
- package/dist/modules/configs/lib/system-status.js +9 -0
- package/dist/modules/configs/lib/system-status.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +3 -1
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +3 -1
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +3 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +3 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customers/ai-tools/companies-pack.js +4 -1
- package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/deals-pack.js +4 -1
- package/dist/modules/customers/ai-tools/deals-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/people-pack.js +4 -1
- package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
- package/dist/modules/customers/api/deals/map/route.js +2 -1
- package/dist/modules/customers/api/deals/map/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +3 -1
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +5 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +3 -1
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/CustomerTodosTable.js +1 -0
- package/dist/modules/customers/components/CustomerTodosTable.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/page.js +3 -1
- package/dist/modules/devices/backend/devices/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/page.js +2 -1
- package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/page.js +2 -1
- package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
- package/dist/modules/entities/api/records.js +4 -2
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +3 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/plots/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/plots/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/statements/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/statements/page.js.map +2 -2
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js +1 -0
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js.map +2 -2
- package/dist/modules/feature_toggles/components/OverridesTable.js +1 -0
- package/dist/modules/feature_toggles/components/OverridesTable.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +3 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +3 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +4 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +3 -1
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js +3 -0
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/push_notifications/backend/push_notifications/page.js +3 -1
- package/dist/modules/push_notifications/backend/push_notifications/page.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +108 -48
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/page.js +3 -1
- package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/page.js +3 -1
- package/dist/modules/resources/backend/resources/resources/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +3 -0
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +3 -0
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +3 -0
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +4 -0
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/page.js +3 -0
- package/dist/modules/staff/backend/staff/leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/my-leave-requests/page.js +3 -0
- package/dist/modules/staff/backend/staff/my-leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/page.js +3 -0
- package/dist/modules/staff/backend/staff/team-members/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/page.js +3 -0
- package/dist/modules/staff/backend/staff/team-roles/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +3 -0
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/page.js +3 -0
- package/dist/modules/staff/backend/staff/teams/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js +3 -0
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/page.js +3 -0
- package/dist/modules/warranty_claims/backend/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +3 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +2 -2
- package/dist/modules/wms/commands/inventory-actions.js +3 -0
- package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js +4 -0
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js +1 -0
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotsListPage.js +1 -0
- package/dist/modules/wms/components/backend/WmsLotsListPage.js.map +2 -2
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js +27 -20
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +3 -1
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/dist/modules/workflows/backend/events/page.js +3 -1
- package/dist/modules/workflows/backend/events/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/page.js +3 -1
- package/dist/modules/workflows/backend/instances/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/page.js +3 -1
- package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/api_keys/backend/api-keys/page.tsx +4 -1
- package/src/modules/attachments/components/AttachmentLibrary.tsx +3 -0
- package/src/modules/audit_logs/backend/audit-logs/page.tsx +8 -0
- package/src/modules/auth/backend/roles/page.tsx +4 -2
- package/src/modules/auth/backend/users/page.tsx +3 -2
- package/src/modules/business_rules/backend/logs/page.tsx +4 -1
- package/src/modules/business_rules/backend/rules/page.tsx +4 -1
- package/src/modules/business_rules/backend/sets/page.tsx +4 -1
- package/src/modules/catalog/ai-agents.ts +1 -1
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +2 -2
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +12 -5
- package/src/modules/catalog/components/categories/CategoriesDataTable.tsx +3 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +7 -0
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/ko.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +4 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +4 -0
- package/src/modules/configs/i18n/de.json +2 -0
- package/src/modules/configs/i18n/en.json +2 -0
- package/src/modules/configs/i18n/es.json +2 -0
- package/src/modules/configs/i18n/ko.json +2 -0
- package/src/modules/configs/i18n/pl.json +2 -0
- package/src/modules/configs/lib/system-status.ts +9 -0
- package/src/modules/currencies/backend/currencies/page.tsx +4 -1
- package/src/modules/currencies/backend/exchange-rates/page.tsx +4 -1
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +4 -1
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +4 -1
- package/src/modules/customers/ai-tools/companies-pack.ts +4 -1
- package/src/modules/customers/ai-tools/deals-pack.ts +4 -1
- package/src/modules/customers/ai-tools/people-pack.ts +4 -1
- package/src/modules/customers/api/deals/map/route.ts +1 -0
- package/src/modules/customers/backend/customers/companies/page.tsx +4 -1
- package/src/modules/customers/backend/customers/deals/page.tsx +8 -2
- package/src/modules/customers/backend/customers/people/page.tsx +4 -1
- package/src/modules/customers/components/CustomerTodosTable.tsx +2 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +4 -1
- package/src/modules/devices/backend/devices/page.tsx +4 -1
- package/src/modules/directory/backend/directory/organizations/page.tsx +3 -1
- package/src/modules/directory/backend/directory/tenants/page.tsx +3 -1
- package/src/modules/entities/api/records.ts +2 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +4 -1
- package/src/modules/eudr/backend/eudr/evidence-submissions/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/plots/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/product-mappings/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/statements/page.tsx +4 -0
- package/src/modules/feature_toggles/components/FeatureTogglesTable.tsx +2 -1
- package/src/modules/feature_toggles/components/OverridesTable.tsx +2 -1
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +4 -0
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +4 -0
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +4 -0
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +4 -1
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +4 -0
- package/src/modules/push_notifications/backend/push_notifications/page.tsx +4 -1
- package/src/modules/query_index/lib/engine.ts +156 -53
- package/src/modules/resources/backend/resources/resource-types/page.tsx +4 -1
- package/src/modules/resources/backend/resources/resources/page.tsx +4 -1
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +4 -0
- package/src/modules/sales/backend/sales/channels/page.tsx +4 -0
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +4 -0
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +5 -0
- package/src/modules/staff/backend/staff/leave-requests/page.tsx +4 -0
- package/src/modules/staff/backend/staff/my-leave-requests/page.tsx +4 -0
- package/src/modules/staff/backend/staff/team-members/page.tsx +6 -0
- package/src/modules/staff/backend/staff/team-roles/page.tsx +4 -0
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +4 -0
- package/src/modules/staff/backend/staff/teams/page.tsx +4 -0
- package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.tsx +4 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +4 -0
- package/src/modules/wms/commands/inventory-actions.ts +3 -0
- package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +5 -0
- package/src/modules/wms/components/backend/WmsInventoryConsolePage.tsx +2 -0
- package/src/modules/wms/components/backend/WmsLotsListPage.tsx +2 -0
- package/src/modules/wms/components/backend/inventoryMutationLoaders.ts +36 -20
- package/src/modules/workflows/backend/definitions/page.tsx +4 -1
- package/src/modules/workflows/backend/events/page.tsx +4 -1
- package/src/modules/workflows/backend/instances/page.tsx +4 -1
- package/src/modules/workflows/backend/tasks/page.tsx +4 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QueryEngine, QueryOptions, QueryResult, QueryResultMeta, EncryptedSortRowCapWarning, FilterOp, Filter, QueryCustomFieldSource, PartialIndexWarning, QueryExtensionsConfig, Sort } from '@open-mercato/shared/lib/query/types'
|
|
1
|
+
import type { QueryEngine, QueryOptions, QueryResult, QueryResultMeta, EncryptedSortRowCapWarning, ListCountCapWarning, FilterOp, Filter, QueryCustomFieldSource, PartialIndexWarning, QueryExtensionsConfig, Sort } from '@open-mercato/shared/lib/query/types'
|
|
2
2
|
import { SortDir } from '@open-mercato/shared/lib/query/types'
|
|
3
3
|
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
4
4
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
@@ -34,6 +34,7 @@ import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
|
|
|
34
34
|
import { runBeforeQueryPipeline, runAfterQueryPipeline, type QueryExtensionContext } from '@open-mercato/shared/lib/query/query-extension-runner'
|
|
35
35
|
import { warnOnCiphertextLikeFallback } from '@open-mercato/shared/lib/query/ciphertext-search-warning'
|
|
36
36
|
import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from '@open-mercato/shared/lib/query/encrypted-sort'
|
|
37
|
+
import { resolveListCountCap } from '@open-mercato/shared/lib/query/count-cap'
|
|
37
38
|
import { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'
|
|
38
39
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
39
40
|
import { parseNumberWithDefault } from '@open-mercato/shared/lib/number'
|
|
@@ -844,10 +845,9 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
844
845
|
return next
|
|
845
846
|
}
|
|
846
847
|
|
|
847
|
-
// Also used by the
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
// `canOptimizeCount` is false whenever any cf filter exists (see `hasCustomFieldFilters`).
|
|
848
|
+
// Also used by the count shape: cf leaves (and doc-based base leaves) reference
|
|
849
|
+
// the index rowset aliases, so any group containing one routes the whole
|
|
850
|
+
// disjunction inside the seeded-rowset EXISTS, where those aliases exist.
|
|
851
851
|
const applyOrGroupedBaseFilters = (q: AnyBuilder): AnyBuilder => {
|
|
852
852
|
if (orGroupFilters.length === 0 && orGroupCfFilters.length === 0) return q
|
|
853
853
|
// `BaseFilter` here is just the normalized-leaf shape; the `cf` bucket holds
|
|
@@ -996,6 +996,101 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
996
996
|
const hasCustomFieldFilters = cfFilters.length > 0
|
|
997
997
|
const canOptimizeCount = !hasCustomFieldFilters && !hasNonBaseSearchSource
|
|
998
998
|
|
|
999
|
+
// ── Count shape (#4552 Phase 2) ─────────────────────────────────
|
|
1000
|
+
// The count query is rebuilt rather than derived from the display shape:
|
|
1001
|
+
// base scope + filters only. Predicates that read the left-joined index
|
|
1002
|
+
// rowset (cf filters, doc filters, or-groups with doc members, token
|
|
1003
|
+
// searches over joined sources) evaluate against the *same* rowset as the
|
|
1004
|
+
// display query, but confined inside a single correlated EXISTS built
|
|
1005
|
+
// from a one-row seed — so the per-base-row rowset is identical, yet a
|
|
1006
|
+
// semi-join cannot multiply base rows. The count therefore needs no
|
|
1007
|
+
// DISTINCT or GROUP BY, and an outer LIMIT actually bounds the scan (no
|
|
1008
|
+
// blocking aggregate between the LIMIT and the scan).
|
|
1009
|
+
const isRowsetDependentFilter = (filter: BaseFilter): boolean => {
|
|
1010
|
+
const baseField = resolveBaseColumn(String(filter.field))
|
|
1011
|
+
if (!baseField) return true
|
|
1012
|
+
// Token search may probe joined sources' record ids, which only exist
|
|
1013
|
+
// on the index rowset.
|
|
1014
|
+
if (
|
|
1015
|
+
(filter.op === 'like' || filter.op === 'ilike') &&
|
|
1016
|
+
searchRuntime.enabled &&
|
|
1017
|
+
typeof filter.value === 'string' &&
|
|
1018
|
+
hasNonBaseSearchSource
|
|
1019
|
+
) return true
|
|
1020
|
+
return false
|
|
1021
|
+
}
|
|
1022
|
+
const rowsetRegularFilters = regularBaseFilters.filter((filter) => isRowsetDependentFilter(filter))
|
|
1023
|
+
const outerRegularFilters = regularBaseFilters.filter((filter) => !isRowsetDependentFilter(filter))
|
|
1024
|
+
// Or-groups are disjuncts of one $or, so they cannot be split between the
|
|
1025
|
+
// outer query and the rowset — one doc-dependent member moves them all in.
|
|
1026
|
+
const orGroupsRowsetDependent =
|
|
1027
|
+
orGroupCfFilters.length > 0 ||
|
|
1028
|
+
orGroupFilters.some((filter) => isRowsetDependentFilter(filter))
|
|
1029
|
+
const hasInnerTypedCfSource = preparedCfSources.some((source) =>
|
|
1030
|
+
((opts.customFieldSources ?? []).find((s) => s && (s.alias ?? undefined) === source.alias)?.join?.type ?? 'left') === 'inner')
|
|
1031
|
+
const needsIndexRowset =
|
|
1032
|
+
hasCustomFieldFilters ||
|
|
1033
|
+
rowsetRegularFilters.length > 0 ||
|
|
1034
|
+
orGroupsRowsetDependent ||
|
|
1035
|
+
hasInnerTypedCfSource
|
|
1036
|
+
|
|
1037
|
+
const applyCountShape = async (q: AnyBuilder): Promise<AnyBuilder> => {
|
|
1038
|
+
let next = applyBaseScope(q)
|
|
1039
|
+
for (const filter of outerRegularFilters) {
|
|
1040
|
+
const baseField = resolveBaseColumn(String(filter.field))
|
|
1041
|
+
if (!baseField) continue
|
|
1042
|
+
next = this.applyColumnFilter(next, qualify(baseField), filter, {
|
|
1043
|
+
...searchRuntime, entity, field: String(filter.field), recordIdColumn: qualify('id'),
|
|
1044
|
+
})
|
|
1045
|
+
}
|
|
1046
|
+
if (!orGroupsRowsetDependent) next = applyOrGroupedBaseFilters(next)
|
|
1047
|
+
if (needsIndexRowset) {
|
|
1048
|
+
// One seed row per base row, then the display query's own join
|
|
1049
|
+
// builders — the rowset inside the EXISTS is the display rowset.
|
|
1050
|
+
// Built from `db` (not the where-callback's expression builder),
|
|
1051
|
+
// matching applyJoinFilters' detached-subquery idiom; correlated
|
|
1052
|
+
// references to the outer base alias resolve at compile time.
|
|
1053
|
+
let rowset: AnyBuilder = db
|
|
1054
|
+
.selectFrom(sql`(select 1)`.as('om_count_seed'))
|
|
1055
|
+
.select(sql<number>`1`.as('one'))
|
|
1056
|
+
rowset = applyEntityIndexesJoin(rowset)
|
|
1057
|
+
rowset = applyCustomFieldSourceJoins(rowset)
|
|
1058
|
+
rowset = applyCfFilters(rowset)
|
|
1059
|
+
for (const filter of rowsetRegularFilters) {
|
|
1060
|
+
const baseField = resolveBaseColumn(String(filter.field))
|
|
1061
|
+
if (!baseField) {
|
|
1062
|
+
rowset = this.applyIndexDocFilterFromAlias(
|
|
1063
|
+
rowset, 'ei', entity, String(filter.field), filter.op, filter.value, qualify('id'), searchRuntime,
|
|
1064
|
+
)
|
|
1065
|
+
continue
|
|
1066
|
+
}
|
|
1067
|
+
rowset = this.applyColumnFilter(rowset, qualify(baseField), filter, {
|
|
1068
|
+
...searchRuntime, entity, field: String(filter.field), recordIdColumn: qualify('id'),
|
|
1069
|
+
})
|
|
1070
|
+
}
|
|
1071
|
+
if (orGroupsRowsetDependent) rowset = applyOrGroupedBaseFilters(rowset)
|
|
1072
|
+
const capturedRowset = rowset
|
|
1073
|
+
next = next.where((eb: any) => eb.exists(capturedRowset))
|
|
1074
|
+
}
|
|
1075
|
+
next = await applyJoinFilters({
|
|
1076
|
+
db,
|
|
1077
|
+
baseTable,
|
|
1078
|
+
builder: next,
|
|
1079
|
+
joinMap,
|
|
1080
|
+
joinFilters,
|
|
1081
|
+
aliasTables,
|
|
1082
|
+
qualifyBase: (column) => qualify(column),
|
|
1083
|
+
applyAliasScope: async (target: any, alias: string) => applyAliasScopes(target as AnyBuilder, alias),
|
|
1084
|
+
applyFilterOp: (target, column, op, value) => applyJoinFilterOpFn(target as AnyBuilder, column, op, value),
|
|
1085
|
+
applyJoinFilterOp: async (target, filter, qualified, join) => {
|
|
1086
|
+
const applied = await applyJoinSearchFilterOp(target as AnyBuilder, filter, qualified, join)
|
|
1087
|
+
return { applied, builder: target }
|
|
1088
|
+
},
|
|
1089
|
+
columnExists: (tbl, column) => this.columnExists(tbl, column),
|
|
1090
|
+
})
|
|
1091
|
+
return next
|
|
1092
|
+
}
|
|
1093
|
+
|
|
999
1094
|
// Selection (for data query)
|
|
1000
1095
|
const selectFieldSet = new Set<string>((opts.fields && opts.fields.length) ? opts.fields.map(String) : Array.from(columns.keys()))
|
|
1001
1096
|
if (requiresPlaintextSort) {
|
|
@@ -1056,33 +1151,23 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1056
1151
|
const pageSize = opts.page?.pageSize ?? 20
|
|
1057
1152
|
const sqlDebugEnabled = this.isSqlDebugEnabled()
|
|
1058
1153
|
|
|
1059
|
-
|
|
1154
|
+
const countCap = resolveListCountCap()
|
|
1060
1155
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
applyAliasScope: async (target: any, alias: string) => applyAliasScopes(target as AnyBuilder, alias),
|
|
1077
|
-
applyFilterOp: (target, column, op, value) => applyJoinFilterOpFn(target as AnyBuilder, column, op, value),
|
|
1078
|
-
applyJoinFilterOp: async (target, filter, qualified, join) => {
|
|
1079
|
-
const applied = await applyJoinSearchFilterOp(target as AnyBuilder, filter, qualified, join)
|
|
1080
|
-
return { applied, builder: target }
|
|
1081
|
-
},
|
|
1082
|
-
columnExists: (tbl, column) => this.columnExists(tbl, column),
|
|
1083
|
-
})
|
|
1084
|
-
const sub = countCore.select(sql.ref(qualify('id')).as('id')).groupBy(qualify('id')).as('sq')
|
|
1085
|
-
const countQuery = db.selectFrom(sub as any).select(sql<string>`count(*)`.as('count'))
|
|
1156
|
+
// Both count paths build the same rebuilt shape; for `canOptimizeCount`
|
|
1157
|
+
// queries `needsIndexRowset` is false, so the shape degenerates to
|
|
1158
|
+
// base-scope + filters exactly as the optimized path always had.
|
|
1159
|
+
// `wasOptimizable` carries NO control flow — it only labels the debug
|
|
1160
|
+
// timing so operators can keep telling the two shapes apart.
|
|
1161
|
+
// TODO(2026-08-12): remove `canOptimizeCount` once the shape convergence
|
|
1162
|
+
// has soaked (tracked in the #4552 spec as the Phase 2 follow-up).
|
|
1163
|
+
const runBoundedCount = async (wasOptimizable: boolean): Promise<{ total: number; warning?: ListCountCapWarning }> => {
|
|
1164
|
+
const countRoot = db.selectFrom(`${baseTable} as b` as any)
|
|
1165
|
+
const shape = await applyCountShape(countRoot)
|
|
1166
|
+
const countQuery = countCap !== null
|
|
1167
|
+
? db
|
|
1168
|
+
.selectFrom(shape.select(sql<number>`1`.as('one')).limit(countCap + 1).as('om_count_probe') as any)
|
|
1169
|
+
.select(sql<string>`count(*)`.as('count'))
|
|
1170
|
+
: shape.select(sql<string>`count(*)`.as('count'))
|
|
1086
1171
|
if (debugEnabled && sqlDebugEnabled) {
|
|
1087
1172
|
const compiled = countQuery.compile()
|
|
1088
1173
|
this.debug('query:sql:count', { entity, sql: compiled.sql, bindings: compiled.parameters })
|
|
@@ -1090,25 +1175,19 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1090
1175
|
const countRow = await this.captureSqlTiming(
|
|
1091
1176
|
'query:sql:count', entity,
|
|
1092
1177
|
() => countQuery.executeTakeFirst(),
|
|
1093
|
-
{ optimized:
|
|
1178
|
+
{ optimized: wasOptimizable }, profiler,
|
|
1094
1179
|
)
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const countBuilder = (await applyQueryShape(countRoot))
|
|
1099
|
-
.select(sql<string>`count(distinct ${sql.ref(qualify('id'))})`.as('count'))
|
|
1100
|
-
if (debugEnabled && sqlDebugEnabled) {
|
|
1101
|
-
const compiled = countBuilder.compile()
|
|
1102
|
-
this.debug('query:sql:count', { entity, sql: compiled.sql, bindings: compiled.parameters })
|
|
1180
|
+
const probed = this.parseCount(countRow)
|
|
1181
|
+
if (countCap !== null && probed > countCap) {
|
|
1182
|
+
return { total: countCap, warning: { entity, cap: countCap } }
|
|
1103
1183
|
}
|
|
1104
|
-
|
|
1105
|
-
'query:sql:count', entity,
|
|
1106
|
-
() => countBuilder.executeTakeFirst(),
|
|
1107
|
-
{ optimized: false }, profiler,
|
|
1108
|
-
)
|
|
1109
|
-
total = this.parseCount(countRow)
|
|
1184
|
+
return { total: probed }
|
|
1110
1185
|
}
|
|
1111
1186
|
|
|
1187
|
+
const counted = await runBoundedCount(canOptimizeCount)
|
|
1188
|
+
const total: number = counted.total
|
|
1189
|
+
const listCountCapWarning: ListCountCapWarning | undefined = counted.warning
|
|
1190
|
+
|
|
1112
1191
|
const dekKeyCache = new Map<string | null, string | null>()
|
|
1113
1192
|
|
|
1114
1193
|
const decryptRow = async (item: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
|
@@ -1159,24 +1238,29 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1159
1238
|
const sortFieldNames = Array.from(new Set(['id', ...scopeFieldNames, ...resolvedSorts.map((s) => String(s.field))]))
|
|
1160
1239
|
phase1 = applySelection(phase1, sortFieldNames)
|
|
1161
1240
|
if (cap !== null) {
|
|
1162
|
-
|
|
1241
|
+
// Probe one row past the cap: truncation is detected from the candidate
|
|
1242
|
+
// scan itself, not by comparing against `total` — which may itself be
|
|
1243
|
+
// capped (`OM_LIST_COUNT_CAP`) and would then never exceed the sort cap.
|
|
1244
|
+
phase1 = phase1.limit(cap + 1).orderBy(qualify('id'), 'asc' as any)
|
|
1163
1245
|
}
|
|
1164
1246
|
if (debugEnabled && sqlDebugEnabled) {
|
|
1165
1247
|
const compiled = phase1.compile()
|
|
1166
1248
|
this.debug('query:sql:data:phase1', { entity, sql: compiled.sql, bindings: compiled.parameters })
|
|
1167
1249
|
}
|
|
1168
|
-
const
|
|
1250
|
+
const candidateRowsRaw = await this.captureSqlTiming(
|
|
1169
1251
|
'query:sql:data:phase1', entity,
|
|
1170
1252
|
() => phase1.execute(),
|
|
1171
1253
|
{ phase: 1 }, profiler,
|
|
1172
1254
|
) as Record<string, unknown>[]
|
|
1255
|
+
const sortTruncated = cap !== null && candidateRowsRaw.length > cap
|
|
1256
|
+
const candidateRows = sortTruncated && cap !== null ? candidateRowsRaw.slice(0, cap) : candidateRowsRaw
|
|
1173
1257
|
const decryptedCandidates = await mapWithConcurrency(candidateRows, DECRYPT_CONCURRENCY, decryptRow)
|
|
1174
1258
|
const orderedCandidates = sortRowsInMemory(decryptedCandidates, resolvedSorts)
|
|
1175
1259
|
const pageIds = orderedCandidates
|
|
1176
1260
|
.slice((page - 1) * pageSize, page * pageSize)
|
|
1177
1261
|
.map((row) => row.id)
|
|
1178
1262
|
|
|
1179
|
-
if (cap !== null
|
|
1263
|
+
if (sortTruncated && cap !== null) {
|
|
1180
1264
|
encryptedSortRowCapWarning = {
|
|
1181
1265
|
entity,
|
|
1182
1266
|
sortFields: resolvedSorts.map((s) => String(s.field)),
|
|
@@ -1232,10 +1316,11 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1232
1316
|
|
|
1233
1317
|
const typedItems = items as unknown as T[]
|
|
1234
1318
|
let result: QueryResult<T> = { items: typedItems, page, pageSize, total }
|
|
1235
|
-
if (partialIndexWarning || encryptedSortRowCapWarning) {
|
|
1319
|
+
if (partialIndexWarning || encryptedSortRowCapWarning || listCountCapWarning) {
|
|
1236
1320
|
const meta: QueryResultMeta = {}
|
|
1237
1321
|
if (partialIndexWarning) meta.partialIndexWarning = partialIndexWarning
|
|
1238
1322
|
if (encryptedSortRowCapWarning) meta.encryptedSortRowCapWarning = encryptedSortRowCapWarning
|
|
1323
|
+
if (listCountCapWarning) meta.listCountCapWarning = listCountCapWarning
|
|
1239
1324
|
result.meta = meta
|
|
1240
1325
|
}
|
|
1241
1326
|
|
|
@@ -2036,17 +2121,35 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
2036
2121
|
const page = opts.page?.page ?? 1
|
|
2037
2122
|
const pageSize = opts.page?.pageSize ?? 20
|
|
2038
2123
|
|
|
2124
|
+
// Single-table count: `applyScope` adds only WHERE predicates (cf filters
|
|
2125
|
+
// included — no join), and doc storage holds one row per record within a
|
|
2126
|
+
// scope, so `count(*)` needs no DISTINCT and — when the cap is active — a
|
|
2127
|
+
// LIMIT on the row-producing inner query bounds the scan (#4552 Phase 2).
|
|
2128
|
+
const countCap = resolveListCountCap()
|
|
2039
2129
|
const root = db.selectFrom(`custom_entities_storage as ${alias}`)
|
|
2040
|
-
const
|
|
2130
|
+
const countShape = applyScope(root)
|
|
2131
|
+
const countQuery = countCap !== null
|
|
2132
|
+
? db
|
|
2133
|
+
.selectFrom(countShape.select(sql<number>`1`.as('one')).limit(countCap + 1).as('om_count_probe') as any)
|
|
2134
|
+
.select(sql<string>`count(*)`.as('count'))
|
|
2135
|
+
: countShape.select(sql<string>`count(*)`.as('count'))
|
|
2041
2136
|
const countRow = await countQuery.executeTakeFirst()
|
|
2042
|
-
const
|
|
2137
|
+
const probed = this.parseCount(countRow)
|
|
2138
|
+
let total = probed
|
|
2139
|
+
let listCountCapWarning: ListCountCapWarning | undefined
|
|
2140
|
+
if (countCap !== null && probed > countCap) {
|
|
2141
|
+
total = countCap
|
|
2142
|
+
listCountCapWarning = { entity: entity as EntityId, cap: countCap }
|
|
2143
|
+
}
|
|
2043
2144
|
|
|
2044
2145
|
let dataQuery = applyScope(db.selectFrom(`custom_entities_storage as ${alias}`))
|
|
2045
2146
|
dataQuery = applySelection(dataQuery)
|
|
2046
2147
|
dataQuery = applySort(dataQuery)
|
|
2047
2148
|
dataQuery = dataQuery.limit(pageSize).offset((page - 1) * pageSize)
|
|
2048
2149
|
const items = await dataQuery.execute()
|
|
2049
|
-
|
|
2150
|
+
const result: QueryResult<T> = { items, page, pageSize, total }
|
|
2151
|
+
if (listCountCapWarning) result.meta = { listCountCapWarning }
|
|
2152
|
+
return result
|
|
2050
2153
|
}
|
|
2051
2154
|
|
|
2052
2155
|
private async tableExists(table: string): Promise<boolean> {
|
|
@@ -45,6 +45,7 @@ type ResourceTypesResponse = {
|
|
|
45
45
|
items?: Array<Record<string, unknown>>
|
|
46
46
|
total?: number
|
|
47
47
|
totalPages?: number
|
|
48
|
+
totalIsCapped?: boolean
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
type ResourceTypesMutationContext = {
|
|
@@ -63,6 +64,7 @@ export default function ResourcesResourceTypesPage() {
|
|
|
63
64
|
const [page, setPage] = React.useState(1)
|
|
64
65
|
const [total, setTotal] = React.useState(0)
|
|
65
66
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
67
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
66
68
|
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'name', desc: false }])
|
|
67
69
|
const [search, setSearch] = React.useState('')
|
|
68
70
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
@@ -232,6 +234,7 @@ export default function ResourcesResourceTypesPage() {
|
|
|
232
234
|
setRows(items.map(mapApiResourceType))
|
|
233
235
|
setTotal(typeof payload.total === 'number' ? payload.total : items.length)
|
|
234
236
|
setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))
|
|
237
|
+
setTotalIsCapped(payload?.totalIsCapped === true)
|
|
235
238
|
} catch (error) {
|
|
236
239
|
logger.error('Failed to list resource types', { err: error })
|
|
237
240
|
flash(translations.errors.load, 'error')
|
|
@@ -308,7 +311,7 @@ export default function ResourcesResourceTypesPage() {
|
|
|
308
311
|
sortable
|
|
309
312
|
sorting={sorting}
|
|
310
313
|
onSortingChange={setSorting}
|
|
311
|
-
pagination={{ page, pageSize: PAGE_SIZE, total, totalPages, onPageChange: setPage }}
|
|
314
|
+
pagination={{ page, pageSize: PAGE_SIZE, total, totalPages, totalIsCapped, onPageChange: setPage }}
|
|
312
315
|
rowActions={(row) => (
|
|
313
316
|
<RowActions
|
|
314
317
|
items={[
|
|
@@ -63,6 +63,7 @@ type ResourcesResponse = {
|
|
|
63
63
|
total: number
|
|
64
64
|
page: number
|
|
65
65
|
totalPages: number
|
|
66
|
+
totalIsCapped?: boolean
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
type ResourceTypesResponse = {
|
|
@@ -81,6 +82,7 @@ export default function ResourcesResourcesPage() {
|
|
|
81
82
|
const [page, setPage] = React.useState(1)
|
|
82
83
|
const [total, setTotal] = React.useState(0)
|
|
83
84
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
85
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
84
86
|
const [search, setSearch] = React.useState('')
|
|
85
87
|
const [filterValues, setFilterValues] = React.useState<FilterValues>({})
|
|
86
88
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
@@ -360,6 +362,7 @@ export default function ResourcesResourcesPage() {
|
|
|
360
362
|
setRows(mapped)
|
|
361
363
|
setTotal(payload.total || 0)
|
|
362
364
|
setTotalPages(payload.totalPages || 1)
|
|
365
|
+
setTotalIsCapped(payload?.totalIsCapped === true)
|
|
363
366
|
}
|
|
364
367
|
} catch (error) {
|
|
365
368
|
if (!cancelled) {
|
|
@@ -546,7 +549,7 @@ export default function ResourcesResourcesPage() {
|
|
|
546
549
|
createLabel={t('resources.resources.list.actions.create', 'New resource')}
|
|
547
550
|
/>
|
|
548
551
|
)}
|
|
549
|
-
pagination={{ page, pageSize: PAGE_SIZE, total, totalPages, onPageChange: setPage }}
|
|
552
|
+
pagination={{ page, pageSize: PAGE_SIZE, total, totalPages, totalIsCapped, onPageChange: setPage }}
|
|
550
553
|
isLoading={isLoading}
|
|
551
554
|
/>
|
|
552
555
|
</PageBody>
|
|
@@ -29,6 +29,7 @@ type OffersResponse = {
|
|
|
29
29
|
items?: Array<Record<string, unknown>>
|
|
30
30
|
total?: number
|
|
31
31
|
totalPages?: number
|
|
32
|
+
totalIsCapped?: boolean
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const PAGE_SIZE = 25
|
|
@@ -61,6 +62,7 @@ export default function SalesChannelOffersListPage() {
|
|
|
61
62
|
const [page, setPage] = React.useState(1)
|
|
62
63
|
const [total, setTotal] = React.useState(0)
|
|
63
64
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
65
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
64
66
|
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'updatedAt', desc: true }])
|
|
65
67
|
const [search, setSearch] = React.useState('')
|
|
66
68
|
const [filterValues, setFilterValues] = React.useState<FilterValues>({})
|
|
@@ -271,6 +273,7 @@ export default function SalesChannelOffersListPage() {
|
|
|
271
273
|
setRows(mapped)
|
|
272
274
|
setTotal(typeof payload.total === 'number' ? payload.total : items.length)
|
|
273
275
|
setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))
|
|
276
|
+
setTotalIsCapped(payload?.totalIsCapped === true)
|
|
274
277
|
const ids = mapped
|
|
275
278
|
.map((row) => row.channelId)
|
|
276
279
|
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
@@ -362,6 +365,7 @@ export default function SalesChannelOffersListPage() {
|
|
|
362
365
|
pageSize: PAGE_SIZE,
|
|
363
366
|
total,
|
|
364
367
|
totalPages,
|
|
368
|
+
totalIsCapped,
|
|
365
369
|
onPageChange: setPage,
|
|
366
370
|
}}
|
|
367
371
|
refreshButton={{
|
|
@@ -37,6 +37,7 @@ type ChannelsResponse = {
|
|
|
37
37
|
items?: Array<Record<string, unknown>>
|
|
38
38
|
total?: number
|
|
39
39
|
totalPages?: number
|
|
40
|
+
totalIsCapped?: boolean
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const PAGE_SIZE = 25
|
|
@@ -68,6 +69,7 @@ export default function SalesChannelsPage() {
|
|
|
68
69
|
const [page, setPage] = React.useState(1)
|
|
69
70
|
const [total, setTotal] = React.useState(0)
|
|
70
71
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
72
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
71
73
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
72
74
|
const [search, setSearch] = React.useState('')
|
|
73
75
|
const [isLoading, setLoading] = React.useState(true)
|
|
@@ -142,6 +144,7 @@ export default function SalesChannelsPage() {
|
|
|
142
144
|
setRows(items.map(mapApiChannel))
|
|
143
145
|
setTotal(typeof payload.total === 'number' ? payload.total : items.length)
|
|
144
146
|
setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))
|
|
147
|
+
setTotalIsCapped(payload?.totalIsCapped === true)
|
|
145
148
|
} catch (err) {
|
|
146
149
|
logger.error('sales.channels.list', { err })
|
|
147
150
|
flash(t('sales.channels.table.errors.load', 'Failed to load channels.'), 'error')
|
|
@@ -230,6 +233,7 @@ export default function SalesChannelsPage() {
|
|
|
230
233
|
pageSize: PAGE_SIZE,
|
|
231
234
|
total,
|
|
232
235
|
totalPages,
|
|
236
|
+
totalIsCapped,
|
|
233
237
|
onPageChange: setPage,
|
|
234
238
|
}}
|
|
235
239
|
refreshButton={{
|
|
@@ -25,6 +25,7 @@ type OffersResponse = {
|
|
|
25
25
|
items?: Array<Record<string, unknown>>
|
|
26
26
|
total?: number
|
|
27
27
|
totalPages?: number
|
|
28
|
+
totalIsCapped?: boolean
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const PAGE_SIZE = 25
|
|
@@ -54,6 +55,7 @@ export function SalesChannelOffersPanel({ channelId, channelName }: { channelId:
|
|
|
54
55
|
const [page, setPage] = React.useState(1)
|
|
55
56
|
const [total, setTotal] = React.useState(0)
|
|
56
57
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
58
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
57
59
|
const [search, setSearch] = React.useState('')
|
|
58
60
|
const [isLoading, setLoading] = React.useState(true)
|
|
59
61
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
@@ -134,6 +136,7 @@ export function SalesChannelOffersPanel({ channelId, channelName }: { channelId:
|
|
|
134
136
|
setRows(items.map(mapOfferRow))
|
|
135
137
|
setTotal(typeof payload.total === 'number' ? payload.total : items.length)
|
|
136
138
|
setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))
|
|
139
|
+
setTotalIsCapped(payload?.totalIsCapped === true)
|
|
137
140
|
} catch (err) {
|
|
138
141
|
logger.error('sales.channels.offers', { err })
|
|
139
142
|
flash(t('sales.channels.offers.errors.load', 'Failed to load offers.'), 'error')
|
|
@@ -208,6 +211,7 @@ export function SalesChannelOffersPanel({ channelId, channelName }: { channelId:
|
|
|
208
211
|
pageSize: PAGE_SIZE,
|
|
209
212
|
total,
|
|
210
213
|
totalPages,
|
|
214
|
+
totalIsCapped,
|
|
211
215
|
onPageChange: setPage,
|
|
212
216
|
}}
|
|
213
217
|
refreshButton={{
|
|
@@ -71,6 +71,7 @@ type DocumentsResponse = {
|
|
|
71
71
|
items?: Array<Record<string, unknown>>
|
|
72
72
|
total?: number
|
|
73
73
|
totalPages?: number
|
|
74
|
+
totalIsCapped?: boolean
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
type SalesDocumentRow = {
|
|
@@ -173,6 +174,7 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
|
|
|
173
174
|
const [page, setPage] = React.useState(1)
|
|
174
175
|
const [total, setTotal] = React.useState(0)
|
|
175
176
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
177
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
176
178
|
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'createdAt', desc: true }])
|
|
177
179
|
const [search, setSearch] = React.useState('')
|
|
178
180
|
const [filterValues, setFilterValues] = React.useState<FilterValues>({})
|
|
@@ -564,6 +566,7 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
|
|
|
564
566
|
setRows([])
|
|
565
567
|
setTotal(0)
|
|
566
568
|
setTotalPages(1)
|
|
569
|
+
setTotalIsCapped(false)
|
|
567
570
|
return
|
|
568
571
|
}
|
|
569
572
|
const payload = call.result ?? {}
|
|
@@ -575,6 +578,7 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
|
|
|
575
578
|
? payload.totalPages
|
|
576
579
|
: Math.max(1, Math.ceil(count / PAGE_SIZE))
|
|
577
580
|
setTotalPages(pages)
|
|
581
|
+
setTotalIsCapped(payload.totalIsCapped === true)
|
|
578
582
|
setCacheStatus(call.cacheStatus ?? null)
|
|
579
583
|
} catch (err) {
|
|
580
584
|
logger.error('sales.documents.list', { err })
|
|
@@ -800,6 +804,7 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
|
|
|
800
804
|
pageSize: PAGE_SIZE,
|
|
801
805
|
total,
|
|
802
806
|
totalPages,
|
|
807
|
+
totalIsCapped,
|
|
803
808
|
onPageChange: setPage,
|
|
804
809
|
cacheStatus,
|
|
805
810
|
}}
|
|
@@ -30,6 +30,7 @@ type LeaveRequestsResponse = {
|
|
|
30
30
|
items?: Array<Record<string, unknown>>
|
|
31
31
|
total?: number
|
|
32
32
|
totalPages?: number
|
|
33
|
+
totalIsCapped?: boolean
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export default function StaffLeaveRequestsPage() {
|
|
@@ -40,6 +41,7 @@ export default function StaffLeaveRequestsPage() {
|
|
|
40
41
|
const [page, setPage] = React.useState(1)
|
|
41
42
|
const [total, setTotal] = React.useState(0)
|
|
42
43
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
44
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
43
45
|
const [search, setSearch] = React.useState('')
|
|
44
46
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
45
47
|
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'startDate', desc: true }])
|
|
@@ -123,6 +125,7 @@ export default function StaffLeaveRequestsPage() {
|
|
|
123
125
|
setRows(items.map(mapLeaveRequest))
|
|
124
126
|
setTotal(typeof payload.total === 'number' ? payload.total : items.length)
|
|
125
127
|
setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)
|
|
128
|
+
setTotalIsCapped(payload?.totalIsCapped === true)
|
|
126
129
|
} catch {
|
|
127
130
|
setRows([])
|
|
128
131
|
setTotal(0)
|
|
@@ -177,6 +180,7 @@ export default function StaffLeaveRequestsPage() {
|
|
|
177
180
|
pageSize: PAGE_SIZE,
|
|
178
181
|
total,
|
|
179
182
|
totalPages,
|
|
183
|
+
totalIsCapped,
|
|
180
184
|
onPageChange: setPage,
|
|
181
185
|
}}
|
|
182
186
|
onRowClick={(row) => {
|
|
@@ -29,6 +29,7 @@ type LeaveRequestsResponse = {
|
|
|
29
29
|
total?: number
|
|
30
30
|
totalPages?: number
|
|
31
31
|
viewer?: { memberId?: string | null; canSend?: boolean }
|
|
32
|
+
totalIsCapped?: boolean
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export default function StaffMyLeaveRequestsPage() {
|
|
@@ -39,6 +40,7 @@ export default function StaffMyLeaveRequestsPage() {
|
|
|
39
40
|
const [page, setPage] = React.useState(1)
|
|
40
41
|
const [total, setTotal] = React.useState(0)
|
|
41
42
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
43
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
42
44
|
const [search, setSearch] = React.useState('')
|
|
43
45
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
44
46
|
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'startDate', desc: true }])
|
|
@@ -118,6 +120,7 @@ export default function StaffMyLeaveRequestsPage() {
|
|
|
118
120
|
setRows(items.map(mapLeaveRequest))
|
|
119
121
|
setTotal(typeof payload.total === 'number' ? payload.total : items.length)
|
|
120
122
|
setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)
|
|
123
|
+
setTotalIsCapped(payload?.totalIsCapped === true)
|
|
121
124
|
const viewerMemberId = typeof payload.viewer?.memberId === 'string' ? payload.viewer.memberId : null
|
|
122
125
|
setMemberId(viewerMemberId)
|
|
123
126
|
setCanSend(payload.viewer?.canSend === true)
|
|
@@ -182,6 +185,7 @@ export default function StaffMyLeaveRequestsPage() {
|
|
|
182
185
|
pageSize: PAGE_SIZE,
|
|
183
186
|
total,
|
|
184
187
|
totalPages,
|
|
188
|
+
totalIsCapped,
|
|
185
189
|
onPageChange: setPage,
|
|
186
190
|
}}
|
|
187
191
|
onRowClick={(row) => {
|
|
@@ -47,6 +47,7 @@ type TeamMembersResponse = {
|
|
|
47
47
|
items?: Array<Record<string, unknown>>
|
|
48
48
|
total?: number
|
|
49
49
|
totalPages?: number
|
|
50
|
+
totalIsCapped?: boolean
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
type TeamsResponse = {
|
|
@@ -68,6 +69,7 @@ export default function StaffTeamMembersPage() {
|
|
|
68
69
|
const [page, setPage] = React.useState(1)
|
|
69
70
|
const [total, setTotal] = React.useState(0)
|
|
70
71
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
72
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
71
73
|
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'displayName', desc: false }])
|
|
72
74
|
const [search, setSearch] = React.useState('')
|
|
73
75
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
@@ -272,6 +274,9 @@ export default function StaffTeamMembersPage() {
|
|
|
272
274
|
: typeof payload.totalPages === 'number'
|
|
273
275
|
? payload.totalPages
|
|
274
276
|
: Math.max(1, Math.ceil(items.length / PAGE_SIZE)))
|
|
277
|
+
// A role filter is applied client-side over the fetched page, so the total
|
|
278
|
+
// shown is that array's length — exact by construction, never a capped floor.
|
|
279
|
+
setTotalIsCapped(!roleFilterApplied && payload.totalIsCapped === true)
|
|
275
280
|
} catch (error) {
|
|
276
281
|
logger.error('staff.team-members.list', { err: error })
|
|
277
282
|
flash(labels.errors.load, 'error')
|
|
@@ -452,6 +457,7 @@ export default function StaffTeamMembersPage() {
|
|
|
452
457
|
pageSize: PAGE_SIZE,
|
|
453
458
|
total,
|
|
454
459
|
totalPages,
|
|
460
|
+
totalIsCapped,
|
|
455
461
|
onPageChange: setPage,
|
|
456
462
|
}}
|
|
457
463
|
rowActions={(row) => row.kind === 'member' ? (
|
|
@@ -41,6 +41,7 @@ type TeamRolesResponse = {
|
|
|
41
41
|
items?: Array<Record<string, unknown>>
|
|
42
42
|
total?: number
|
|
43
43
|
totalPages?: number
|
|
44
|
+
totalIsCapped?: boolean
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
type TeamsResponse = {
|
|
@@ -56,6 +57,7 @@ export default function StaffTeamRolesPage() {
|
|
|
56
57
|
const [page, setPage] = React.useState(1)
|
|
57
58
|
const [total, setTotal] = React.useState(0)
|
|
58
59
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
60
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
59
61
|
const [search, setSearch] = React.useState('')
|
|
60
62
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
61
63
|
const [reloadToken, setReloadToken] = React.useState(0)
|
|
@@ -203,6 +205,7 @@ export default function StaffTeamRolesPage() {
|
|
|
203
205
|
setRows(buildTeamRoleRows(items.map(mapApiTeamRole), labels.groups.unassigned))
|
|
204
206
|
setTotal(typeof payload.total === 'number' ? payload.total : items.length)
|
|
205
207
|
setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))
|
|
208
|
+
setTotalIsCapped(payload?.totalIsCapped === true)
|
|
206
209
|
} catch (error) {
|
|
207
210
|
logger.error('staff.team-roles.list', { err: error })
|
|
208
211
|
flash(labels.errors.load, 'error')
|
|
@@ -329,6 +332,7 @@ export default function StaffTeamRolesPage() {
|
|
|
329
332
|
pageSize: PAGE_SIZE,
|
|
330
333
|
total,
|
|
331
334
|
totalPages,
|
|
335
|
+
totalIsCapped,
|
|
332
336
|
onPageChange: setPage,
|
|
333
337
|
}}
|
|
334
338
|
rowActions={(row) => row.kind === 'role' ? (
|