@mohasinac/appkit 4.46.0 → 4.48.0
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/.build-stamp.json +4 -4
- package/dist/_internal/server/features/checkout/expand-order-items.js +3 -0
- package/dist/_internal/server/features/messages/actions.d.ts +68 -0
- package/dist/_internal/server/features/messages/actions.js +249 -0
- package/dist/_internal/server/features/parts/buildable.d.ts +47 -0
- package/dist/_internal/server/features/parts/buildable.js +159 -0
- package/dist/_internal/server/features/products/list-public.d.ts +17 -0
- package/dist/_internal/server/features/products/list-public.js +92 -5
- package/dist/_internal/server/features/products/og.d.ts +7 -0
- package/dist/_internal/server/features/products/og.js +38 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +6 -0
- package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +28 -0
- package/dist/_internal/server/jobs/core/notificationPrune.js +87 -3
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +36 -1
- package/dist/_internal/shared/actions/action-registry.js +8 -0
- package/dist/_internal/shared/features/notifications/action-url.js +8 -0
- package/dist/_internal/shared/features/notifications/email-eligibility.js +3 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +4 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +8 -0
- package/dist/constants/api-endpoints.d.ts +48 -0
- package/dist/constants/api-endpoints.js +30 -0
- package/dist/constants/notification-types.d.ts +1 -1
- package/dist/features/about/components/GstExplainerView.d.ts +28 -0
- package/dist/features/about/components/GstExplainerView.js +59 -0
- package/dist/features/about/components/GuidesHubView.d.ts +28 -0
- package/dist/features/about/components/GuidesHubView.js +58 -0
- package/dist/features/about/components/PublicProfileView.js +5 -0
- package/dist/features/about/components/SpotAFakeView.d.ts +29 -0
- package/dist/features/about/components/SpotAFakeView.js +49 -0
- package/dist/features/admin/actions/notification-actions.js +4 -0
- package/dist/features/admin/schemas/firestore.d.ts +13 -3
- package/dist/features/admin/schemas/firestore.js +8 -0
- package/dist/features/blog/repository/blog.repository.js +21 -7
- package/dist/features/categories/api/route.js +41 -1
- package/dist/features/categories/components/BreadcrumbTrail.d.ts +58 -13
- package/dist/features/categories/components/BreadcrumbTrail.js +63 -26
- package/dist/features/categories/components/CategoryDetailPageView.js +92 -38
- package/dist/features/categories/components/ContextualTrail.d.ts +62 -0
- package/dist/features/categories/components/ContextualTrail.js +81 -0
- package/dist/features/categories/components/LastCategoryBeacon.d.ts +18 -0
- package/dist/features/categories/components/LastCategoryBeacon.js +33 -0
- package/dist/features/categories/repository/categories.repository.d.ts +21 -0
- package/dist/features/categories/repository/categories.repository.js +36 -3
- package/dist/features/categories/repository/price-index.repository.d.ts +25 -0
- package/dist/features/email/notification-templates.js +4 -0
- package/dist/features/events/repository/events.repository.js +10 -3
- package/dist/features/faq/constants/read-caps.d.ts +29 -0
- package/dist/features/faq/constants/read-caps.js +29 -0
- package/dist/features/faq/repository/faqs.repository.d.ts +0 -2
- package/dist/features/faq/repository/faqs.repository.js +19 -16
- package/dist/features/homepage/components/AdSlot.js +10 -4
- package/dist/features/homepage/hooks/useActiveAd.d.ts +18 -1
- package/dist/features/homepage/hooks/useActiveAd.js +42 -2
- package/dist/features/homepage/lib/section-renderer.js +12 -2
- package/dist/features/layout/AppLayoutShell.d.ts +4 -1
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +4 -1
- package/dist/features/layout/TitleBarLayout.js +8 -4
- package/dist/features/media/AvatarUpload.js +8 -1
- package/dist/features/media/MediaAvatar.js +1 -1
- package/dist/features/media/MediaImage.js +3 -1
- package/dist/features/media/MediaLightbox.js +4 -1
- package/dist/features/media/MediaPickerModal.js +6 -1
- package/dist/features/media/MediaVideo.js +3 -1
- package/dist/features/messages/components/ConversationThreadView.d.ts +3 -0
- package/dist/features/messages/components/ConversationThreadView.js +75 -0
- package/dist/features/messages/components/ConversationsInboxView.d.ts +1 -0
- package/dist/features/messages/components/ConversationsInboxView.js +39 -0
- package/dist/features/messages/components/MessageSellerButton.d.ts +17 -0
- package/dist/features/messages/components/MessageSellerButton.js +33 -0
- package/dist/features/messages/hooks/useConversationChannel.d.ts +24 -0
- package/dist/features/messages/hooks/useConversationChannel.js +89 -0
- package/dist/features/messages/hooks/useMessages.d.ts +347 -0
- package/dist/features/messages/hooks/useMessages.js +113 -0
- package/dist/features/messages/repository/conversations.repository.d.ts +44 -0
- package/dist/features/messages/repository/conversations.repository.js +173 -0
- package/dist/features/messages/repository/messages.repository.d.ts +42 -0
- package/dist/features/messages/repository/messages.repository.js +142 -0
- package/dist/features/messages/schemas/firestore.d.ts +99 -0
- package/dist/features/messages/schemas/firestore.js +46 -0
- package/dist/features/orders/repository/orders.repository.js +21 -7
- package/dist/features/orders/schemas/firestore.d.ts +11 -0
- package/dist/features/products/components/CityCollectiblesView.d.ts +31 -0
- package/dist/features/products/components/CityCollectiblesView.js +82 -0
- package/dist/features/products/components/MarketPriceAnchor.d.ts +45 -0
- package/dist/features/products/components/MarketPriceAnchor.js +62 -0
- package/dist/features/products/components/ProductDetailPageView.js +71 -5
- package/dist/features/products/constants/meetup-cities.d.ts +47 -0
- package/dist/features/products/constants/meetup-cities.js +49 -0
- package/dist/features/products/repository/category-assignment.d.ts +82 -11
- package/dist/features/products/repository/category-assignment.js +93 -12
- package/dist/features/products/repository/products.repository.d.ts +12 -1
- package/dist/features/products/repository/products.repository.js +66 -34
- package/dist/features/products/schemas/firestore.d.ts +24 -2
- package/dist/features/products/schemas/firestore.js +13 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/promotions/repository/coupons.repository.js +11 -4
- package/dist/features/reviews/repository/reviews.repository.js +11 -4
- package/dist/features/scams/repository/scammer.repository.js +21 -7
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +17 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +14 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +27 -2
- package/dist/features/seller/components/SellerProductShell.js +32 -3
- package/dist/features/seller/repository/offer.repository.js +21 -6
- package/dist/features/seller/schemas/product-form.d.ts +3 -3
- package/dist/features/seller/schemas/product-form.js +1 -1
- package/dist/features/server-errors/repository/server-errors.repository.js +11 -0
- package/dist/features/server-errors/schemas/firestore.d.ts +17 -0
- package/dist/features/server-errors/schemas/firestore.js +2 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +17 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +31 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +1 -1
- package/dist/features/store-extensions/schemas/firestore.js +4 -0
- package/dist/features/stores/repository/store.repository.js +11 -3
- package/dist/features/tester/seed-data/authored/community-support__messages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/community-support__messages.js +200 -0
- package/dist/features/tester/seed-data/authored/index.js +2 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +64 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +1 -1
- package/dist/next/routing/route-map.d.ts +14 -0
- package/dist/next/routing/route-map.js +26 -0
- package/dist/providers/db-firebase/rtdb-paths.d.ts +8 -0
- package/dist/providers/db-firebase/rtdb-paths.js +8 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +4 -0
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-schemas.d.ts +17 -0
- package/dist/security/pii-schemas.js +15 -7
- package/dist/seed/_helpers/category-content-generated.d.ts +24 -0
- package/dist/seed/_helpers/category-content-generated.js +1853 -0
- package/dist/seed/_helpers/category-models.d.ts +1 -1
- package/dist/seed/_helpers/category-models.js +25 -1
- package/dist/seed/_helpers/category-names.d.ts +54 -0
- package/dist/seed/_helpers/category-names.js +118 -0
- package/dist/seed/_helpers/character-features.d.ts +1 -1
- package/dist/seed/_helpers/character-features.js +5 -4
- package/dist/seed/_helpers/final-sale-fixtures.js +1 -1
- package/dist/seed/categories-seed-data.js +55 -1
- package/dist/seed/grouped-listings-seed-data.js +2 -2
- package/dist/seed/products-art-seed-data.js +2 -1
- package/dist/seed/products-auctions-seed-data.js +42 -22
- package/dist/seed/products-classifieds-seed-data.js +21 -3
- package/dist/seed/products-digital-codes-seed-data.js +2 -1
- package/dist/seed/products-live-items-seed-data.js +2 -1
- package/dist/seed/products-preorders-seed-data.js +26 -11
- package/dist/seed/products-prize-draws-seed-data.js +8 -1
- package/dist/seed/products-standard-seed-data.js +26 -25
- package/dist/seed/products-stickers-seed-data.js +12 -11
- package/dist/server.d.ts +8 -0
- package/dist/server.js +15 -0
- package/dist/styles.css +113 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/sunset.js +17 -0
- package/dist/tokens/tokens.css +5 -0
- package/dist/ui/components/Breadcrumb.d.ts +8 -1
- package/dist/ui/components/Breadcrumb.js +2 -2
- package/dist/ui/components/Breadcrumb.style.css +37 -0
- package/dist/ui/components/Details.d.ts +11 -1
- package/dist/ui/components/Details.js +2 -2
- package/dist/ui/components/ImageMosaic.d.ts +43 -0
- package/dist/ui/components/ImageMosaic.js +46 -0
- package/dist/ui/components/ImageMosaic.style.css +69 -0
- package/dist/ui/components/SplitTile.d.ts +34 -0
- package/dist/ui/components/SplitTile.js +28 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +4 -0
- package/dist/utils/search-txt-query.d.ts +77 -0
- package/dist/utils/search-txt-query.js +50 -28
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { increment } from "../../../contracts/field-ops.js";
|
|
|
6
6
|
import { withHistory } from "../../../_internal/shared/history/index.js";
|
|
7
7
|
import { SCAMMER_TRACKED_FIELDS, SCAMMER_HISTORY_PII_FIELDS } from "../schemas/firestore.js";
|
|
8
8
|
import { buildScammerSearchTxt } from "../../../utils/search-txt-builders.js";
|
|
9
|
-
import { planSearchTxt,
|
|
9
|
+
import { planSearchTxt, beginSearchWindow, emptySearchResult, } from "../../../utils/search-txt-query.js";
|
|
10
10
|
import { SCAMMER_COLLECTION, SCAMMER_FIELDS, SCAMMER_INCIDENTS_SUBCOLLECTION, SCAMMER_COMMENTS_SUBCOLLECTION, DEFAULT_SCAMMER_DATA, } from "../schemas/firestore.js";
|
|
11
11
|
class ScammerRepository extends BaseRepository {
|
|
12
12
|
constructor() {
|
|
@@ -35,12 +35,19 @@ class ScammerRepository extends BaseRepository {
|
|
|
35
35
|
if (plan.head) {
|
|
36
36
|
baseQuery = baseQuery.where(SCAMMER_FIELDS.SEARCH_TXT, "array-contains", plan.head);
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
/*
|
|
39
|
+
* One bounded window, one raised `maxPageSize`, one page slice — see
|
|
40
|
+
* `beginSearchWindow`. Hand-writing the three is how the other ten
|
|
41
|
+
* repositories each ended up with two of them, which reads as a working
|
|
42
|
+
* multi-word search and silently cannot reach page 2.
|
|
43
|
+
*/
|
|
44
|
+
const win = beginSearchWindow(model, plan);
|
|
45
|
+
const result = await this.sieveQuery(win.model, ScammerRepository.SIEVE_FIELDS, {
|
|
39
46
|
baseQuery,
|
|
40
47
|
defaultPageSize: 20,
|
|
41
|
-
maxPageSize: 50,
|
|
48
|
+
maxPageSize: win.maxPageSize(50),
|
|
42
49
|
});
|
|
43
|
-
return
|
|
50
|
+
return win.finish(result);
|
|
44
51
|
}
|
|
45
52
|
/** Derived on every write path via `applyWriteHooks`. */
|
|
46
53
|
buildSearchTxtFor(data) {
|
|
@@ -56,12 +63,19 @@ class ScammerRepository extends BaseRepository {
|
|
|
56
63
|
if (plan.head) {
|
|
57
64
|
baseQuery = baseQuery.where(SCAMMER_FIELDS.SEARCH_TXT, "array-contains", plan.head);
|
|
58
65
|
}
|
|
59
|
-
|
|
66
|
+
/*
|
|
67
|
+
* One bounded window, one raised `maxPageSize`, one page slice — see
|
|
68
|
+
* `beginSearchWindow`. Hand-writing the three is how the other ten
|
|
69
|
+
* repositories each ended up with two of them, which reads as a working
|
|
70
|
+
* multi-word search and silently cannot reach page 2.
|
|
71
|
+
*/
|
|
72
|
+
const win = beginSearchWindow(model, plan);
|
|
73
|
+
const result = await this.sieveQuery(win.model, ScammerRepository.SIEVE_FIELDS, {
|
|
60
74
|
baseQuery,
|
|
61
75
|
defaultPageSize: 50,
|
|
62
|
-
maxPageSize: 200,
|
|
76
|
+
maxPageSize: win.maxPageSize(200),
|
|
63
77
|
});
|
|
64
|
-
return
|
|
78
|
+
return win.finish(result);
|
|
65
79
|
}
|
|
66
80
|
async findBySeoSlug(seoSlug) {
|
|
67
81
|
try {
|
|
@@ -6,5 +6,21 @@ export interface CategoryInlineSelectProps {
|
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
/** Show the "+ Create new category" option. Defaults to true — sellers can create categories inline. */
|
|
8
8
|
allowCreate?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* I2 — the ADDITIONAL assignments, beyond `value`.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 Omitting this pair keeps the component exactly single-select, which is
|
|
13
|
+
* what every existing call site gets. Multi-assignment is opt-in per surface
|
|
14
|
+
* rather than switched on globally, because `value`/`onChange` is a `string`
|
|
15
|
+
* contract that five call sites rely on and changing it under them is Root
|
|
16
|
+
* Cause #20 (a public prop change without its consumers in the same commit).
|
|
17
|
+
*/
|
|
18
|
+
extraValues?: string[];
|
|
19
|
+
onExtraValuesChange?: (ids: string[]) => void;
|
|
20
|
+
/**
|
|
21
|
+
* How many assignments are allowed in total, `value` included. Defaults to
|
|
22
|
+
* `MAX_CATEGORY_ASSIGNMENTS`, which is what the repository refuses above.
|
|
23
|
+
*/
|
|
24
|
+
maxAssignments?: number;
|
|
9
25
|
}
|
|
10
|
-
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
26
|
+
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, extraValues, onExtraValuesChange, maxAssignments, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
|
|
4
4
|
import { CategoryQuickCreateForm } from "../../admin/components/CategoryQuickCreateForm.js";
|
|
5
5
|
import { apiClient } from "../../../http/index.js";
|
|
6
6
|
import { ADMIN_ENDPOINTS, CATEGORY_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
7
|
+
import { Stack } from "../../../ui/components/Layout.js";
|
|
8
|
+
import { Text } from "../../../ui/components/Typography.js";
|
|
9
|
+
import { MAX_CATEGORY_ASSIGNMENTS } from "../../products/repository/category-assignment.js";
|
|
7
10
|
// Admin: paginated + server-side search via /api/admin/categories (admin/mod only).
|
|
8
11
|
// Response via successResponse({ data: items, total }) → apiClient unwraps to { data, total, ... }.
|
|
9
12
|
async function loadAdminCategoryOptions(query, page) {
|
|
@@ -35,9 +38,17 @@ async function loadPublicCategoryOptions(query, _page) {
|
|
|
35
38
|
const items = q ? all.filter((c) => c.label.toLowerCase().includes(q)) : all;
|
|
36
39
|
return { items, hasMore: false, nextPage: 2 };
|
|
37
40
|
}
|
|
38
|
-
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate = true, }) {
|
|
41
|
+
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate = true, extraValues, onExtraValuesChange, maxAssignments = MAX_CATEGORY_ASSIGNMENTS, }) {
|
|
39
42
|
const loadOptions = allowCreate ? loadAdminCategoryOptions : loadPublicCategoryOptions;
|
|
40
|
-
|
|
43
|
+
const primary = (_jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadOptions, placeholder: placeholder, disabled: disabled, createLabel: "category", drawerTitle: "Create Category", renderCreateForm: allowCreate
|
|
41
44
|
? ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel }))
|
|
42
45
|
: undefined }));
|
|
46
|
+
// Single-select unless the caller opted in — byte-identical to before.
|
|
47
|
+
if (!onExtraValuesChange)
|
|
48
|
+
return primary;
|
|
49
|
+
const extras = extraValues ?? [];
|
|
50
|
+
const remaining = Math.max(0, maxAssignments - 1 - extras.length);
|
|
51
|
+
return (_jsxs(Stack, { gap: "sm", children: [primary, _jsx(PaginatedSelect, { multiple: true, value: extras, onChange: (v) => onExtraValuesChange(Array.isArray(v) ? v : []), loadOptions: loadOptions, placeholder: remaining > 0
|
|
52
|
+
? `Also list under… (up to ${remaining} more)`
|
|
53
|
+
: "No more categories can be added", disabled: disabled || remaining <= 0 }), _jsx(Text, { size: "xs", color: "muted", children: "Optional. Add a category from a DIFFERENT tree \u2014 a franchise, for example. Two categories from the same tree will be refused, because the broader one is already implied by the narrower." })] }));
|
|
43
54
|
}
|
|
@@ -6,6 +6,16 @@ export interface SellerProductDraft {
|
|
|
6
6
|
slug?: string;
|
|
7
7
|
description?: string;
|
|
8
8
|
category?: string;
|
|
9
|
+
/**
|
|
10
|
+
* I2 — the assignment list, DEFAULT FIRST, with `category` mirroring `[0]`.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 Declared on the TYPE as well as the Zod schema. The schema is
|
|
13
|
+
* `.passthrough()`, so an undeclared key survives at runtime — and stays
|
|
14
|
+
* untyped, so the picker could not read `values.categoryIds` at all. Both
|
|
15
|
+
* halves are needed, which is the same trap as a field missing from
|
|
16
|
+
* `PRODUCT_UPDATABLE_FIELDS` (Root Cause #101) one layer out.
|
|
17
|
+
*/
|
|
18
|
+
categoryIds?: string[];
|
|
9
19
|
brand?: string;
|
|
10
20
|
condition?: string;
|
|
11
21
|
tags?: string[];
|
|
@@ -43,7 +53,7 @@ export interface SellerProductDraft {
|
|
|
43
53
|
video?: MediaField;
|
|
44
54
|
youtubeId?: string;
|
|
45
55
|
price?: number;
|
|
46
|
-
|
|
56
|
+
originalPrice?: number;
|
|
47
57
|
stockQuantity?: number;
|
|
48
58
|
featured?: boolean;
|
|
49
59
|
isPromoted?: boolean;
|
|
@@ -134,10 +144,25 @@ export interface SellerProductShellProps {
|
|
|
134
144
|
onDiscard?: () => void;
|
|
135
145
|
isLoading?: boolean;
|
|
136
146
|
storeSlug?: string;
|
|
137
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Render a custom category selector (e.g. PaginatedSelect with createLabel).
|
|
149
|
+
*
|
|
150
|
+
* I2 — `extraValues`/`onExtraValuesChange` carry the ADDITIONAL assignments.
|
|
151
|
+
* A selector that ignores them stays exactly single-select, which is what
|
|
152
|
+
* every surface except this one does.
|
|
153
|
+
*/
|
|
138
154
|
renderCategorySelector?: (props: {
|
|
139
155
|
value: string;
|
|
140
156
|
onChange: (v: string) => void;
|
|
157
|
+
/**
|
|
158
|
+
* 🛑 OPTIONAL, and that is the whole point. `QuickProductForm` is a second
|
|
159
|
+
* consumer of this slot and making these required broke it immediately —
|
|
160
|
+
* Root Cause #20, a public prop change without its consumers, which the
|
|
161
|
+
* comment above literally warns about. A selector that ignores them stays
|
|
162
|
+
* exactly single-select.
|
|
163
|
+
*/
|
|
164
|
+
extraValues?: string[];
|
|
165
|
+
onExtraValuesChange?: (ids: string[]) => void;
|
|
141
166
|
}) => React.ReactNode;
|
|
142
167
|
/** Render a custom brand selector. */
|
|
143
168
|
renderBrandSelector?: (props: {
|
|
@@ -46,6 +46,15 @@ const sellerProductSchema = z.object({
|
|
|
46
46
|
description: z.string().max(10000).optional().or(z.literal("")),
|
|
47
47
|
price: z.number().min(0).optional(),
|
|
48
48
|
category: z.string().optional(),
|
|
49
|
+
/**
|
|
50
|
+
* I2 — the assignment list, DEFAULT FIRST. `category` mirrors `[0]`.
|
|
51
|
+
*
|
|
52
|
+
* Declared even though this schema is `.passthrough()`, because the draft's
|
|
53
|
+
* TYPE is what the render site reads `values.categoryIds` off — passthrough
|
|
54
|
+
* keeps an undeclared key at runtime and still leaves it untyped, so the
|
|
55
|
+
* picker could not see it.
|
|
56
|
+
*/
|
|
57
|
+
categoryIds: z.array(z.string()).max(4).optional(),
|
|
49
58
|
brand: z.string().optional(),
|
|
50
59
|
isActive: z.boolean().optional(),
|
|
51
60
|
images: z
|
|
@@ -106,7 +115,27 @@ function StepBasic({ values, onChange, renderCategorySelector, renderBrandSelect
|
|
|
106
115
|
condition: CONDITION_OPTIONS.find((o) => o.value === (values.condition ?? "new"))?.label ?? "",
|
|
107
116
|
siteName: "LetItRip",
|
|
108
117
|
};
|
|
109
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({
|
|
118
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({
|
|
119
|
+
value: values.category ?? "",
|
|
120
|
+
onChange: (v) => onChange({ category: v }),
|
|
121
|
+
/*
|
|
122
|
+
* 🛑 `categoryIds[0]` is the DEFAULT assignment and is already
|
|
123
|
+
* rendered by the primary picker above, so the extras list is
|
|
124
|
+
* everything AFTER it. Passing the whole array would show the
|
|
125
|
+
* primary twice and let a seller "remove" it from a control that
|
|
126
|
+
* does not own it.
|
|
127
|
+
*/
|
|
128
|
+
extraValues: (values.categoryIds ?? []).slice(1),
|
|
129
|
+
onExtraValuesChange: (ids) => onChange({
|
|
130
|
+
/*
|
|
131
|
+
* Rebuilt primary-first on every change, because the server
|
|
132
|
+
* derives the default by DEPTH and reorders the stored array
|
|
133
|
+
* anyway — sending it in a different order would make the
|
|
134
|
+
* form and the saved document disagree until the next load.
|
|
135
|
+
*/
|
|
136
|
+
categoryIds: [values.category ?? "", ...ids].filter(Boolean),
|
|
137
|
+
}),
|
|
138
|
+
})] })) : (_jsx(FormField, { name: "category", label: "Category", type: "text", value: values.category ?? "", onChange: (v) => onChange({ category: v }), placeholder: "e.g. Trading Cards" })), _jsx(FormField, { name: "condition", label: "Condition", type: "select", value: values.condition ?? "new", onChange: (v) => onChange({ condition: v }), options: CONDITION_OPTIONS })] }), renderTemplateSelector?.({
|
|
110
139
|
categoryId: values.category ?? "",
|
|
111
140
|
condition: values.condition ?? "new",
|
|
112
141
|
context: templateContext,
|
|
@@ -359,7 +388,7 @@ const TYPE_SPECIFIC_SECTIONS = {
|
|
|
359
388
|
function StepPricing({ values, onChange, listingType, }) {
|
|
360
389
|
const listingPlugin = pluginForMode(listingType);
|
|
361
390
|
const priceLabel = listingPlugin.priceLabel;
|
|
362
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: "All prices in Indian Rupees (\u20B9)." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "price", label: priceLabel, type: "number", value: toRupeesString(values.price), onChange: (v) => onChange({ price: fromRupeesString(v) }), placeholder: "999" }), _jsx(FormField, { name: "
|
|
391
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: "All prices in Indian Rupees (\u20B9)." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "price", label: priceLabel, type: "number", value: toRupeesString(values.price), onChange: (v) => onChange({ price: fromRupeesString(v) }), placeholder: "999" }), _jsx(FormField, { name: "originalPrice", label: "Compare-at Price (\u20B9, optional)", type: "number", value: toRupeesString(values.originalPrice), onChange: (v) => onChange({ originalPrice: v ? fromRupeesString(v) : undefined }), placeholder: "1299", helpText: "Original price shown as strikethrough" })] }), listingPlugin.showsStockQuantity && (_jsx(FormField, { name: "stockQuantity", label: "Stock Quantity", type: "number", value: String(values.stockQuantity ?? ""), onChange: (v) => onChange({ stockQuantity: Number(v) }), placeholder: "1" })), _jsxs(FormGroup, { columns: 2, children: [_jsx(Toggle, { checked: !!values.featured, onChange: (checked) => onChange({ featured: checked }), label: "Mark as Featured" }), _jsx(Toggle, { checked: !!values.isNew, onChange: (checked) => onChange({ isNew: checked }), label: "Mark as New Arrival" })] }), _jsx(Toggle, { checked: !!values.allowOffers, onChange: (checked) => onChange({
|
|
363
392
|
allowOffers: checked,
|
|
364
393
|
minOfferPercent: checked
|
|
365
394
|
? values.minOfferPercent ?? DEFAULT_MIN_OFFER_PERCENT
|
|
@@ -614,7 +643,7 @@ productId, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCat
|
|
|
614
643
|
required: true,
|
|
615
644
|
quick: true,
|
|
616
645
|
// `gstRate` / `hsnCode` deliberately absent — admin-only, see StepPricing.
|
|
617
|
-
fields: ["price", "
|
|
646
|
+
fields: ["price", "originalPrice", "stockQuantity", "allowOffers", "minOfferPercent"],
|
|
618
647
|
render: ({ values, onChange }) => (_jsx(StepPricing, { values: values, onChange: onChange, listingType: listingType })),
|
|
619
648
|
// Built per render because whether stock is required depends on the
|
|
620
649
|
// listing type's own plugin — a digital code has no stock to speak of.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Data access layer for the `offers` Firestore collection.
|
|
5
5
|
* Handles buyer make-an-offer, seller counter/accept/decline lifecycle.
|
|
6
6
|
*/
|
|
7
|
-
import { planSearchTxt,
|
|
7
|
+
import { planSearchTxt, beginSearchWindow, emptySearchResult, } from "../../../utils/search-txt-query.js";
|
|
8
8
|
import { buildOfferSearchTxt } from "../../../utils/search-txt-builders.js";
|
|
9
9
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase/index.js";
|
|
10
10
|
import { OFFER_COLLECTION, OFFER_FIELDS, OFFER_TRACKED_FIELDS, createOfferId } from "../schemas/index.js";
|
|
@@ -43,12 +43,19 @@ class OfferRepository extends BaseRepository {
|
|
|
43
43
|
if (plan.head) {
|
|
44
44
|
baseQuery = baseQuery.where("searchTxt", "array-contains", plan.head);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
/*
|
|
47
|
+
* One bounded window, one raised `maxPageSize`, one page slice — see
|
|
48
|
+
* `beginSearchWindow`. Hand-writing the three is how the other ten
|
|
49
|
+
* repositories each ended up with two of them, which reads as a working
|
|
50
|
+
* multi-word search and silently cannot reach page 2.
|
|
51
|
+
*/
|
|
52
|
+
const win = beginSearchWindow(model, plan);
|
|
53
|
+
const result = await this.sieveQuery(win.model, OfferRepository.SIEVE_FIELDS, {
|
|
47
54
|
baseQuery,
|
|
48
55
|
defaultPageSize: 50,
|
|
49
|
-
maxPageSize: 200,
|
|
56
|
+
maxPageSize: win.maxPageSize(200),
|
|
50
57
|
});
|
|
51
|
-
return
|
|
58
|
+
return win.finish(result);
|
|
52
59
|
}
|
|
53
60
|
// --- Create --------------------------------------------------------------
|
|
54
61
|
async create(input) {
|
|
@@ -113,10 +120,18 @@ class OfferRepository extends BaseRepository {
|
|
|
113
120
|
if (plan.head) {
|
|
114
121
|
baseQuery = baseQuery.where(OFFER_FIELDS.SEARCH_TXT, "array-contains", plan.head);
|
|
115
122
|
}
|
|
116
|
-
|
|
123
|
+
/*
|
|
124
|
+
* One bounded window, one raised `maxPageSize`, one page slice — see
|
|
125
|
+
* `beginSearchWindow`. Hand-writing the three is how the other ten
|
|
126
|
+
* repositories each ended up with two of them, which reads as a working
|
|
127
|
+
* multi-word search and silently cannot reach page 2.
|
|
128
|
+
*/
|
|
129
|
+
const win = beginSearchWindow(model, plan);
|
|
130
|
+
const result = await this.sieveQuery(win.model, OfferRepository.SIEVE_FIELDS, {
|
|
117
131
|
baseQuery,
|
|
132
|
+
maxPageSize: win.maxPageSize(100),
|
|
118
133
|
});
|
|
119
|
-
return
|
|
134
|
+
return win.finish(result);
|
|
120
135
|
}
|
|
121
136
|
const snap = await this.getCollection()
|
|
122
137
|
.where(OFFER_FIELDS.STORE_ID, "==", storeId)
|
|
@@ -65,7 +65,7 @@ export declare const productDraftSchema: z.ZodObject<{
|
|
|
65
65
|
youtubeId: z.ZodOptional<z.ZodString>;
|
|
66
66
|
externalVideoUrl: z.ZodOptional<z.ZodString>;
|
|
67
67
|
price: z.ZodOptional<z.ZodNumber>;
|
|
68
|
-
|
|
68
|
+
originalPrice: z.ZodOptional<z.ZodNumber>;
|
|
69
69
|
stockQuantity: z.ZodOptional<z.ZodNumber>;
|
|
70
70
|
featured: z.ZodOptional<z.ZodBoolean>;
|
|
71
71
|
isPromoted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -155,7 +155,7 @@ export declare const productDraftSchema: z.ZodObject<{
|
|
|
155
155
|
youtubeId: z.ZodOptional<z.ZodString>;
|
|
156
156
|
externalVideoUrl: z.ZodOptional<z.ZodString>;
|
|
157
157
|
price: z.ZodOptional<z.ZodNumber>;
|
|
158
|
-
|
|
158
|
+
originalPrice: z.ZodOptional<z.ZodNumber>;
|
|
159
159
|
stockQuantity: z.ZodOptional<z.ZodNumber>;
|
|
160
160
|
featured: z.ZodOptional<z.ZodBoolean>;
|
|
161
161
|
isPromoted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -245,7 +245,7 @@ export declare const productDraftSchema: z.ZodObject<{
|
|
|
245
245
|
youtubeId: z.ZodOptional<z.ZodString>;
|
|
246
246
|
externalVideoUrl: z.ZodOptional<z.ZodString>;
|
|
247
247
|
price: z.ZodOptional<z.ZodNumber>;
|
|
248
|
-
|
|
248
|
+
originalPrice: z.ZodOptional<z.ZodNumber>;
|
|
249
249
|
stockQuantity: z.ZodOptional<z.ZodNumber>;
|
|
250
250
|
featured: z.ZodOptional<z.ZodBoolean>;
|
|
251
251
|
isPromoted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -64,7 +64,7 @@ export const productDraftSchema = z
|
|
|
64
64
|
externalVideoUrl: z.string().optional(),
|
|
65
65
|
// Pricing
|
|
66
66
|
price: z.number().optional(),
|
|
67
|
-
|
|
67
|
+
originalPrice: z.number().optional(),
|
|
68
68
|
stockQuantity: z.number().optional(),
|
|
69
69
|
featured: z.boolean().optional(),
|
|
70
70
|
isPromoted: z.boolean().optional(),
|
|
@@ -39,6 +39,17 @@ export class ServerErrorsRepository extends BaseRepository {
|
|
|
39
39
|
requestId: input.requestId,
|
|
40
40
|
...(input.userAgent ? { userAgent: input.userAgent } : {}),
|
|
41
41
|
...(input.bodyDigest ? { bodyDigest: input.bodyDigest } : {}),
|
|
42
|
+
/*
|
|
43
|
+
* 🛑 D10, and the G2 sweep is what found it: this hand-picked
|
|
44
|
+
* field list is exactly Root Cause #38's shape. `entityType` and
|
|
45
|
+
* `entityId` were on `ServerErrorDocument` AND accepted by this
|
|
46
|
+
* method's own `Omit<…>` input type — so a caller passing them
|
|
47
|
+
* type-checked clean and the fields died right here, on the one
|
|
48
|
+
* line nobody re-reads. A write that succeeds is not a field that
|
|
49
|
+
* persists.
|
|
50
|
+
*/
|
|
51
|
+
...(input.entityType ? { entityType: input.entityType } : {}),
|
|
52
|
+
...(input.entityId ? { entityId: input.entityId } : {}),
|
|
42
53
|
};
|
|
43
54
|
await ref.set(doc);
|
|
44
55
|
}
|
|
@@ -31,6 +31,21 @@ export type ServerErrorDocument = {
|
|
|
31
31
|
userAgent?: string;
|
|
32
32
|
/** sha256(requestBody) — for diagnosis without leaking PII. */
|
|
33
33
|
bodyDigest?: string;
|
|
34
|
+
/**
|
|
35
|
+
* D10 — WHAT the failure was about, when that is narrower than `route`.
|
|
36
|
+
*
|
|
37
|
+
* 🛑 Absent means UNKNOWN, never "not a page". Every row written before this
|
|
38
|
+
* field existed has no value, so any filter over it must treat a missing
|
|
39
|
+
* `entityType` as INCLUDED — a default-exclusive filter would hide every
|
|
40
|
+
* pre-existing row and manufacture a backfill nobody budgeted for.
|
|
41
|
+
*
|
|
42
|
+
* `"page"` is the one value a user-submitted report can produce (see
|
|
43
|
+
* `ReportEntityType`), which is what lets an operator line a visitor's
|
|
44
|
+
* "this page is broken" up against the server-side rows for the same route.
|
|
45
|
+
*/
|
|
46
|
+
entityType?: "page" | "product" | "store" | "order" | "listing";
|
|
47
|
+
/** The entity the failure was about — a path for `entityType: "page"`. */
|
|
48
|
+
entityId?: string;
|
|
34
49
|
};
|
|
35
50
|
export declare const SERVER_ERROR_FIELDS: {
|
|
36
51
|
readonly ID: "id";
|
|
@@ -46,6 +61,8 @@ export declare const SERVER_ERROR_FIELDS: {
|
|
|
46
61
|
readonly REQUEST_ID: "requestId";
|
|
47
62
|
readonly USER_AGENT: "userAgent";
|
|
48
63
|
readonly BODY_DIGEST: "bodyDigest";
|
|
64
|
+
readonly ENTITY_TYPE: "entityType";
|
|
65
|
+
readonly ENTITY_ID: "entityId";
|
|
49
66
|
};
|
|
50
67
|
export declare const SERVER_ERROR_STACK_MAX_BYTES = 4096;
|
|
51
68
|
export declare const SERVER_ERROR_RETENTION_DAYS = 30;
|
|
@@ -13,6 +13,8 @@ export const SERVER_ERROR_FIELDS = {
|
|
|
13
13
|
REQUEST_ID: "requestId",
|
|
14
14
|
USER_AGENT: "userAgent",
|
|
15
15
|
BODY_DIGEST: "bodyDigest",
|
|
16
|
+
ENTITY_TYPE: "entityType",
|
|
17
|
+
ENTITY_ID: "entityId",
|
|
16
18
|
};
|
|
17
19
|
export const SERVER_ERROR_STACK_MAX_BYTES = 4096;
|
|
18
20
|
export const SERVER_ERROR_RETENTION_DAYS = 30;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
16
16
|
import type { DocumentSnapshot } from "firebase-admin/firestore";
|
|
17
|
-
import { type AnalyticsAlertDocument, type AnalyticsCardDocument, type ItemRequestDocument, type ListingTemplateDocument, type ModerationQueueDocument, type PayoutMethodDocument, type ReportDocument, type ShippingConfigDocument, type StoreCategoryDocument, type StoreGoogleConfigDocument, type StoreWhatsAppConfigDocument } from "../schemas/firestore";
|
|
17
|
+
import { type AnalyticsAlertDocument, type AnalyticsCardDocument, type ItemRequestDocument, type ItemRequestReply, type ListingTemplateDocument, type ModerationQueueDocument, type PayoutMethodDocument, type ReportDocument, type ShippingConfigDocument, type StoreCategoryDocument, type StoreGoogleConfigDocument, type StoreWhatsAppConfigDocument } from "../schemas/firestore";
|
|
18
18
|
export declare class PayoutMethodsRepository extends BaseRepository<PayoutMethodDocument> {
|
|
19
19
|
/**
|
|
20
20
|
* Bank/UPI details, encrypted at rest by BaseRepository's write hooks.
|
|
@@ -86,6 +86,22 @@ export declare class ItemRequestsRepository extends BaseRepository<ItemRequestDo
|
|
|
86
86
|
listByOwner(opUserId: string): Promise<{
|
|
87
87
|
items: ItemRequestDocument[];
|
|
88
88
|
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Append one reply atomically.
|
|
91
|
+
*
|
|
92
|
+
* 🛑 F1 #9 — the route did `replies: [...doc.replies, entry]` plus
|
|
93
|
+
* `replyCount: doc.replyCount + 1`. That is a read-modify-write on both
|
|
94
|
+
* fields: two people replying within the same second each read N replies and
|
|
95
|
+
* each wrote N+1, so the second write silently DISCARDED the first reply and
|
|
96
|
+
* the count agreed with it. Nothing errored, and the losing reply simply
|
|
97
|
+
* never existed.
|
|
98
|
+
*
|
|
99
|
+
* `arrayUnion` + `increment` are server-side operations, so the two writes
|
|
100
|
+
* compose instead of racing. `arrayUnion` de-duplicates by deep equality,
|
|
101
|
+
* which is harmless here and faintly useful: the entry carries a unique
|
|
102
|
+
* `id`, so a retried request appends once rather than twice.
|
|
103
|
+
*/
|
|
104
|
+
appendReply(id: string, reply: ItemRequestReply): Promise<void>;
|
|
89
105
|
}
|
|
90
106
|
export declare class StoreWhatsAppConfigRepository extends BaseRepository<StoreWhatsAppConfigDocument> {
|
|
91
107
|
constructor();
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* The other ten hold no PII and use the BaseRepository public API only.
|
|
14
14
|
*/
|
|
15
15
|
import { BaseRepository } from "../../../providers/db-firebase/index.js";
|
|
16
|
+
import { arrayUnion, increment } from "../../../contracts/field-ops.js";
|
|
16
17
|
import { decryptPiiFields } from "../../../security/pii-encrypt.js";
|
|
17
18
|
import { ANALYTICS_ALERTS_COLLECTION, ANALYTICS_CARDS_COLLECTION, ITEM_REQUESTS_COLLECTION, LISTING_TEMPLATES_COLLECTION, MODERATION_QUEUE_COLLECTION, PAYOUT_METHODS_COLLECTION, REPORTS_COLLECTION, SHIPPING_CONFIGS_COLLECTION, STORE_CATEGORIES_COLLECTION, STORE_GOOGLE_CONFIG_COLLECTION, STORE_WHATSAPP_CONFIG_COLLECTION, } from "../schemas/firestore.js";
|
|
18
19
|
export class PayoutMethodsRepository extends BaseRepository {
|
|
@@ -117,6 +118,36 @@ export class ItemRequestsRepository extends BaseRepository {
|
|
|
117
118
|
const items = await this.findBy("opUserId", opUserId);
|
|
118
119
|
return { items };
|
|
119
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Append one reply atomically.
|
|
123
|
+
*
|
|
124
|
+
* 🛑 F1 #9 — the route did `replies: [...doc.replies, entry]` plus
|
|
125
|
+
* `replyCount: doc.replyCount + 1`. That is a read-modify-write on both
|
|
126
|
+
* fields: two people replying within the same second each read N replies and
|
|
127
|
+
* each wrote N+1, so the second write silently DISCARDED the first reply and
|
|
128
|
+
* the count agreed with it. Nothing errored, and the losing reply simply
|
|
129
|
+
* never existed.
|
|
130
|
+
*
|
|
131
|
+
* `arrayUnion` + `increment` are server-side operations, so the two writes
|
|
132
|
+
* compose instead of racing. `arrayUnion` de-duplicates by deep equality,
|
|
133
|
+
* which is harmless here and faintly useful: the entry carries a unique
|
|
134
|
+
* `id`, so a retried request appends once rather than twice.
|
|
135
|
+
*/
|
|
136
|
+
async appendReply(id, reply) {
|
|
137
|
+
/*
|
|
138
|
+
* Written through the collection directly rather than `update()`, because
|
|
139
|
+
* `prepareForFirestore` would walk the sentinels as if they were plain
|
|
140
|
+
* values. Same shape `categories.repository` uses for its own
|
|
141
|
+
* `arrayUnion` writes.
|
|
142
|
+
*/
|
|
143
|
+
await this.getCollection()
|
|
144
|
+
.doc(id)
|
|
145
|
+
.update({
|
|
146
|
+
replies: arrayUnion(reply),
|
|
147
|
+
replyCount: increment(1),
|
|
148
|
+
updatedAt: new Date(),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
120
151
|
}
|
|
121
152
|
export class StoreWhatsAppConfigRepository extends BaseRepository {
|
|
122
153
|
constructor() {
|
|
@@ -171,7 +171,7 @@ export interface ModerationQueueDocument extends BaseDocument {
|
|
|
171
171
|
export declare const MODERATION_QUEUE_COLLECTION: "moderationQueue";
|
|
172
172
|
export declare const MODERATION_QUEUE_INDEXED_FIELDS: readonly ["status", "mediaType", "entityType", "entityId", "storeId", "ownerId", "submittedAt", "createdAt"];
|
|
173
173
|
export declare const DEFAULT_MODERATION_QUEUE_DATA: Partial<ModerationQueueDocument>;
|
|
174
|
-
export type ReportEntityType = "product" | "store" | "review" | "event" | "user" | "blog" | "comment";
|
|
174
|
+
export type ReportEntityType = "product" | "store" | "review" | "event" | "user" | "blog" | "comment" | "page" | "message";
|
|
175
175
|
export type ReportStatus = "pending" | "under-review" | "actioned" | "dismissed";
|
|
176
176
|
export type ReportReason = "scam" | "counterfeit" | "prohibited" | "inappropriate" | "harassment" | "spam" | "ip-violation" | "other";
|
|
177
177
|
export interface ReportDocument extends BaseDocument {
|
|
@@ -139,6 +139,10 @@ const REPORT_ENTITY_TYPE_MAP = {
|
|
|
139
139
|
user: true,
|
|
140
140
|
blog: true,
|
|
141
141
|
comment: true,
|
|
142
|
+
// D10 — a broken PAGE. `entityId` carries the path.
|
|
143
|
+
page: true,
|
|
144
|
+
// D9 — a chat message.
|
|
145
|
+
message: true,
|
|
142
146
|
};
|
|
143
147
|
export const REPORT_ENTITY_TYPES = Object.keys(REPORT_ENTITY_TYPE_MAP);
|
|
144
148
|
const REPORT_REASON_MAP = {
|
|
@@ -12,7 +12,7 @@ import { normalizeError } from "../../../errors/normalize.js";
|
|
|
12
12
|
import { encryptSecret, decryptSecret, } from "../../../security/settings-encryption.js";
|
|
13
13
|
import { withHistory } from "../../../_internal/shared/history/index.js";
|
|
14
14
|
import { buildStoreSearchTxt } from "../../../utils/search-txt-builders.js";
|
|
15
|
-
import { planSearchTxt,
|
|
15
|
+
import { planSearchTxt, beginSearchWindow, emptySearchResult, } from "../../../utils/search-txt-query.js";
|
|
16
16
|
export class StoreRepository extends BaseRepository {
|
|
17
17
|
constructor() {
|
|
18
18
|
super(STORE_COLLECTION);
|
|
@@ -220,9 +220,17 @@ export class StoreRepository extends BaseRepository {
|
|
|
220
220
|
if (plan.head) {
|
|
221
221
|
baseQuery = baseQuery.where(STORE_FIELDS.SEARCH_TXT, "array-contains", plan.head);
|
|
222
222
|
}
|
|
223
|
+
/*
|
|
224
|
+
* One bounded window, one raised `maxPageSize`, one page slice — see
|
|
225
|
+
* `beginSearchWindow`. Hand-writing the three is how the other ten
|
|
226
|
+
* repositories each ended up with two of them, which reads as a working
|
|
227
|
+
* multi-word search and silently cannot reach page 2.
|
|
228
|
+
*/
|
|
229
|
+
const win = beginSearchWindow(model, plan);
|
|
223
230
|
const result = await applySieveToFirestore({
|
|
224
231
|
baseQuery,
|
|
225
|
-
model,
|
|
232
|
+
model: win.model,
|
|
233
|
+
options: { maxPageSize: win.maxPageSize(100) },
|
|
226
234
|
fields: sieveFields,
|
|
227
235
|
// Called DIRECTLY, not through `sieveQuery`, so the mapper has to be
|
|
228
236
|
// passed by hand — and it is the list mapper, never `mapDoc`. With
|
|
@@ -230,7 +238,7 @@ export class StoreRepository extends BaseRepository {
|
|
|
230
238
|
// WhatsApp bearer token and a list has no use for it.
|
|
231
239
|
mapDoc: (snap) => this.mapDocForList(snap),
|
|
232
240
|
});
|
|
233
|
-
const searched =
|
|
241
|
+
const searched = win.finish(result);
|
|
234
242
|
/*
|
|
235
243
|
* 🛑 The rating threshold is applied IN MEMORY, deliberately.
|
|
236
244
|
*
|