@mohasinac/appkit 4.32.0 → 4.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,7 +31,12 @@ export function UserBidsView() {
31
31
  const config = {
32
32
  portal: "user",
33
33
  title: "My Bids",
34
- searchPlaceholder: "Search by auction title…",
34
+ search: {
35
+ placeholder: "Search by auction title…",
36
+ // `/api/user/bids` filters in memory: `(b.productTitle ?? "").includes(q)`.
37
+ // The bid document carries no other text.
38
+ fields: ["productTitle"],
39
+ },
35
40
  emptyLabel: "You haven't placed any bids yet.",
36
41
  filterKeys: ["status"],
37
42
  defaultSort: sortBy("bidDate", "DESC"),
@@ -48,7 +48,14 @@ export function UserOrdersView({ onOrderClick }) {
48
48
  const config = {
49
49
  portal: "user",
50
50
  title: "My Orders",
51
- searchPlaceholder: "Search by order id…",
51
+ search: {
52
+ placeholder: "Search by order id…",
53
+ // `/api/user/orders` filters in memory on `o.id` and nothing else — not
54
+ // the product titles the rows display, which is what a buyer would try
55
+ // first. Saying "order id" is the honest promise until the route can
56
+ // match more.
57
+ fields: ["id"],
58
+ },
52
59
  emptyLabel: "You haven't placed any orders yet.",
53
60
  filterKeys: ["status", "orderType"],
54
61
  defaultSort: sortBy("createdAt", "DESC"),
@@ -21,7 +21,12 @@ export function UserReturnsView({ onOrderClick }) {
21
21
  const config = {
22
22
  portal: "user",
23
23
  title: "Returns & Refunds",
24
- searchPlaceholder: "Search by order id…",
24
+ search: {
25
+ placeholder: "Search by order id…",
26
+ // Same route as /user/orders, scoped to returned statuses — so the same
27
+ // single in-memory match on `o.id`.
28
+ fields: ["id"],
29
+ },
25
30
  emptyLabel: "You have no return or refund requests.",
26
31
  filterKeys: [],
27
32
  defaultSort: sortBy("updatedAt", "DESC"),
@@ -50,7 +50,18 @@ export function AdminAuditLogView({ children, ...props }) {
50
50
  const config = {
51
51
  portal: "admin",
52
52
  title: "Audit Log",
53
- searchPlaceholder: "Search by actor uid",
53
+ search: {
54
+ placeholder: "Search by actor uid (exact)",
55
+ /*
56
+ * `/api/admin/audit-log` builds `actorUid==${q}` — an EQUALITY, stated in
57
+ * that route's own comment because `@=` is prefix-only against Firestore
58
+ * anyway. A partial uid matches nothing, so the box commits on Enter and
59
+ * says "exact" rather than letting someone type half a uid and watch an
60
+ * empty list.
61
+ */
62
+ mode: "exact",
63
+ fields: ["actorUid"],
64
+ },
54
65
  emptyLabel: "No audit log entries found",
55
66
  filterKeys: ["action", "actorUid"],
56
67
  defaultSort: sortBy("createdAt", "DESC"),
@@ -62,7 +62,11 @@ export function AdminBundlesView({ editHrefTemplate, newHref }) {
62
62
  const config = {
63
63
  portal: "admin",
64
64
  title: "Bundles",
65
- searchPlaceholder: "Search bundles by name or slug…",
65
+ search: {
66
+ placeholder: "Search bundles by name or slug…",
67
+ // `/api/admin/bundles` filters in memory on `b.name` and `b.slug`.
68
+ fields: ["name", "slug"],
69
+ },
66
70
  emptyLabel: BUNDLE_COPY.adminList.empty,
67
71
  filterKeys: ["isActive", "bundleStockStatus"],
68
72
  defaultSort: sortBy("name", "ASC"),
@@ -33,7 +33,16 @@ export function AdminFaqsView({ children, onBulkArchive, onBulkDelete, ...props
33
33
  const config = useMemo(() => ({
34
34
  portal: "admin",
35
35
  title: "FAQs",
36
- searchPlaceholder: "Search questions, categories, or tokens",
36
+ search: {
37
+ placeholder: "Search questions, answers, categories or tags",
38
+ /*
39
+ * `buildFaqSearchTxt` indexes the question, the category, every tag AND
40
+ * the answer body, all prefix-expanded. The old placeholder said
41
+ * "tokens" — the name of the implementation — and omitted the answer,
42
+ * which is the largest thing it searches.
43
+ */
44
+ fields: ["question", "answer", "category", "tags"],
45
+ },
37
46
  emptyLabel: "No FAQs found",
38
47
  filterKeys: ["isActive"],
39
48
  defaultSort: sortBy("priority", "ASC"),
@@ -81,7 +81,13 @@ export function AdminNotificationsView({ children, ...props }) {
81
81
  // Title search is not expressible here — `title` is not in the repository's
82
82
  // SIEVE_FIELDS and Sieve→Firestore cannot OR across two fields. The route
83
83
  // treats `q` as an exact userId lookup, so the placeholder says exactly that.
84
- searchPlaceholder: "Search by user ID",
84
+ search: {
85
+ placeholder: "Search by user ID (exact)",
86
+ // `/api/admin/notifications` builds `userId==${q}`. An equality, so a
87
+ // partial id matches nothing and the box commits on Enter.
88
+ mode: "exact",
89
+ fields: ["userId"],
90
+ },
85
91
  emptyLabel: "No notifications found",
86
92
  filterKeys: ["type", "readState"],
87
93
  defaultSort: sortBy("createdAt", "DESC"),
@@ -75,7 +75,17 @@ export function AdminPrizeDrawsView({ children, ...props }) {
75
75
  const config = {
76
76
  portal: "admin",
77
77
  title: "Prize Draws",
78
- searchPlaceholder: "Search prize draws by name or store…",
78
+ search: {
79
+ placeholder: "Search prize draws by name, brand or tag…",
80
+ /*
81
+ * 🛑 "or store" was a promise the corpus cannot keep.
82
+ * `buildProductSearchTxt` indexes title, description, brand, brandSlug,
83
+ * category names, tags, features, condition and card/grading details —
84
+ * and NO store name or id. Typing a seller's name returned an empty list
85
+ * that reads as "this seller runs no prize draws".
86
+ */
87
+ fields: ["title", "description", "brand", "categoryNames", "tags", "features"],
88
+ },
79
89
  emptyLabel: "No prize draws found",
80
90
  filterKeys: ["status"],
81
91
  defaultSort: sortBy("createdAt", "DESC"),
@@ -43,7 +43,19 @@ export function AdminStoresView({ children, ...props }) {
43
43
  const config = {
44
44
  portal: "admin",
45
45
  title: "Stores",
46
- searchPlaceholder: "Search stores, slugs, or owner names",
46
+ search: {
47
+ placeholder: "Search stores by name, description or category",
48
+ /*
49
+ * 🛑 "slugs, or owner names" was a promise nothing could keep — neither
50
+ * is in `buildStoreSearchTxt`, and the owner's name lives on another
51
+ * collection entirely.
52
+ *
53
+ * The route now searches `searchTxt` rather than `storeName_=`, so this
54
+ * is genuinely partial across all three fields; it used to be a
55
+ * STARTS-WITH match on the name alone.
56
+ */
57
+ fields: ["storeName", "storeDescription", "storeCategory"],
58
+ },
47
59
  emptyLabel: "No stores found",
48
60
  filterKeys: ["status"],
49
61
  defaultSort: sortBy("createdAt", "DESC"),
@@ -57,7 +57,17 @@ export function AdminSupportTicketsView({ children, ...props }) {
57
57
  const config = {
58
58
  portal: "admin",
59
59
  title: "Support Tickets",
60
- searchPlaceholder: "Search by subject",
60
+ search: {
61
+ placeholder: "Search by subject (starts with)",
62
+ /*
63
+ * `/api/admin/support-tickets` builds `subject@=${q}`, and `@=` is
64
+ * PREFIX-only against Firestore. `searchTxt` on this collection is built
65
+ * from the subject alone on purpose — the description and every message
66
+ * body are PII-encrypted, and indexing them in plaintext would hand back
67
+ * exactly what the encryption withholds.
68
+ */
69
+ fields: ["subject"],
70
+ },
61
71
  emptyLabel: "No support tickets found",
62
72
  filterKeys: ["status", "priority", "showResolved"],
63
73
  defaultSort: sortBy("createdAt", "DESC"),
@@ -57,7 +57,17 @@ export function AdminTeamView({ children, onBulkRemove, ...props }) {
57
57
  const config = {
58
58
  portal: "admin",
59
59
  title: "Team",
60
- searchPlaceholder: "Search by name or email",
60
+ search: {
61
+ placeholder: "Search by name or email",
62
+ /*
63
+ * Genuinely partial, and that is worth stating: `/api/admin/team`
64
+ * filters AFTER `mapDoc` has decrypted, with
65
+ * `(u.email ?? "").toLowerCase().includes(q)`. Most PII search in this
66
+ * codebase resolves an HMAC blind index and matches exactly; this one
67
+ * does not, which is why `mode` stays partial here.
68
+ */
69
+ fields: ["displayName", "email"],
70
+ },
61
71
  emptyLabel: "No employees found",
62
72
  filterKeys: ["group"],
63
73
  defaultSort: sortBy("createdAt", "DESC"),
@@ -54,7 +54,12 @@ export function AdminTesterChecklistView({ children, onBulkDelete, ...props }) {
54
54
  const config = useMemo(() => ({
55
55
  portal: "admin",
56
56
  title: "Tester Checklist",
57
- searchPlaceholder: "Search test cases, groups, or pages",
57
+ search: {
58
+ placeholder: "Search test cases, descriptions, groups or pages",
59
+ // `TesterChecklistItemRepository` builds `searchTxt` from label,
60
+ // description, groupLabel and pageLabel.
61
+ fields: ["label", "description", "groupLabel", "pageLabel"],
62
+ },
58
63
  emptyLabel: "No checklist items found",
59
64
  filterKeys: ["isActive", "bugConfirmed"],
60
65
  // Bug-confirmed (and reopened-away) cases are always isActive:false —
@@ -33,7 +33,12 @@ export function AdminPageViewsReportView() {
33
33
  const config = {
34
34
  portal: "admin",
35
35
  title: "Page Views",
36
- searchPlaceholder: "Search by entity or URL",
36
+ search: {
37
+ placeholder: "Search by entity or URL",
38
+ // `/api/admin/analytics/pageviews` filters in memory on `row.entityId`
39
+ // and `row.url`.
40
+ fields: ["entityId", "url"],
41
+ },
37
42
  emptyLabel: "No page views recorded for this range.",
38
43
  filterKeys: ["entityType"],
39
44
  defaultSort: sortBy("count", "DESC"),
@@ -41,7 +41,11 @@ export function SellerBundlesView({ editHrefTemplate, newHref }) {
41
41
  const config = {
42
42
  portal: "seller",
43
43
  title: "Bundles",
44
- searchPlaceholder: "Search bundles by name or slug…",
44
+ search: {
45
+ placeholder: "Search bundles by name or slug…",
46
+ // `/api/store/bundles` filters in memory on `b.name` and `b.slug`.
47
+ fields: ["name", "slug"],
48
+ },
45
49
  emptyLabel: "No bundles yet — create a bundle to group multiple of your products together",
46
50
  filterKeys: ["isActive", "bundleStockStatus"],
47
51
  defaultSort: sortBy("name", "ASC"),
@@ -212,7 +212,16 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
212
212
  const config = {
213
213
  portal: "seller",
214
214
  title: "Products",
215
- searchPlaceholder: "Search products by name…",
215
+ search: {
216
+ placeholder: "Search by name, description, brand or tag…",
217
+ /*
218
+ * The box understated itself: `/api/store/products` routes `q` through
219
+ * `buildProductSearchTxt`, which indexes far more than the title. A
220
+ * seller searching a brand or a tag got results and had no reason to
221
+ * expect them.
222
+ */
223
+ fields: ["title", "description", "brand", "categoryNames", "tags", "features"],
224
+ },
216
225
  emptyLabel: listingKind !== "all" ? `No ${listingKind} listings found` : "No products listed yet",
217
226
  filterKeys: FILTER_KEYS,
218
227
  defaultSort: DEFAULT_SORT,
@@ -19,13 +19,15 @@ import { PRODUCT_FIELDS } from "../../../constants/field-names";
19
19
  export async function listStores(params = {}, viewer) {
20
20
  const { filters, sorts = "-createdAt", page = 1, pageSize = 24, q } = params;
21
21
  const model = { filters, sorts, page, pageSize };
22
+ // The same fix as the admin route: `q` travels as the searchTxt term, not as
23
+ // `storeName_=`, which was a prefix match on the name alone.
22
24
  const filtersArr = [];
23
- if (q)
24
- filtersArr.push(`storeName_=${q}`);
25
25
  if (filters)
26
26
  filtersArr.push(filters);
27
27
  model.filters = filtersArr.join(",") || undefined;
28
- const result = await storeRepository.listStores(model);
28
+ const result = await storeRepository.listStores(model, true, {
29
+ search: q || undefined,
30
+ });
29
31
  return { ...result, items: filterTestDataForViewer(result.items, viewer) };
30
32
  }
31
33
  /**
@@ -98,7 +98,9 @@ export declare class StoreRepository extends BaseRepository<StoreDocument> {
98
98
  /**
99
99
  * Admin: list all stores regardless of status.
100
100
  */
101
- listAllStores(model: SieveModel): Promise<FirebaseSieveResult<StoreDocument>>;
101
+ listAllStores(model: SieveModel, opts?: {
102
+ search?: string;
103
+ }): Promise<FirebaseSieveResult<StoreDocument>>;
102
104
  /**
103
105
  * Cloud Functions: atomically increment `stats.totalProducts` counter.
104
106
  */
@@ -230,8 +230,12 @@ export class StoreRepository extends BaseRepository {
230
230
  /**
231
231
  * Admin: list all stores regardless of status.
232
232
  */
233
- async listAllStores(model) {
234
- return this.listStores(model, false);
233
+ async listAllStores(model, opts) {
234
+ // 🛑 `opts` is forwarded. It was not, so the admin store list had no way to
235
+ // reach the `searchTxt` path `listStores` already implements — and both its
236
+ // caller and the public one fell back to `storeName_=`, a PREFIX match on
237
+ // the name alone. "Beyblade Arena" was unfindable by "arena".
238
+ return this.listStores(model, false, opts);
235
239
  }
236
240
  /**
237
241
  * Cloud Functions: atomically increment `stats.totalProducts` counter.
@@ -2077,6 +2077,18 @@ const rawTesterChecklistItems = [
2077
2077
  description: "/store/print-center is now a redirect, and it is deep-linked from the orders and products lists as ?type=order&ids=…&autoprint=1. Select two orders on /store/orders, use the print action, and confirm the Print Centre tab opens with those two orders loaded. Arriving at an EMPTY Print Centre is the failure — it looks like a normal empty state, which is why this case names the before.",
2078
2078
  href: "/store/orders",
2079
2079
  },
2080
+ {
2081
+ key: "search-box-promises-match-corpus",
2082
+ label: "Every dashboard search box matches what its placeholder promises",
2083
+ description: "BEFORE: four boxes promised a match the data could not make. /admin/prize-draws, /admin/deals and /admin/featured each read \"or seller\" while the product search index carries no store name at all, and /admin/stores read \"slugs, or owner names\" while the route ran a STARTS-WITH match on the store name alone. Each returned an empty list that reads as \"there are none\", not as \"this box cannot search that\". AFTER: type a seller's name into /admin/prize-draws — the placeholder no longer offers it. On /admin/stores type a word from the MIDDLE of a store's name (\"arena\", for Beyblade Arena) and a word from its description: both must now return it.",
2084
+ href: "/admin/stores",
2085
+ },
2086
+ {
2087
+ key: "exact-search-boxes-commit-on-enter",
2088
+ label: "The two exact-match boxes say so and do not search while you type",
2089
+ description: "/admin/audit-log matches `actorUid` and /admin/notifications matches `userId`, both by EQUALITY — a partial id matches nothing. Both placeholders now say \"(exact)\". Type half a uid: the list must not churn per keystroke, and must not show \"no results\" until you press Enter. Each intermediate query would be a billed read that cannot match.",
2090
+ href: "/admin/audit-log",
2091
+ },
2080
2092
  {
2081
2093
  key: "consolidated-tabs-command-palette",
2082
2094
  label: "The absorbed surfaces are still findable by name in search",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.32.0",
3
+ "version": "4.32.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"