@mohasinac/appkit 3.6.5 → 3.8.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/_internal/client/features/layout/DashboardLayoutClient.d.ts +17 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +7 -2
- package/dist/_internal/client/features/tour/TourProvider.d.ts +5 -2
- package/dist/_internal/client/features/tour/TourProvider.js +125 -12
- package/dist/_internal/client/theme/ThemeProvider.js +1 -1
- package/dist/_internal/server/features/categories/data.js +2 -2
- package/dist/_internal/server/features/faqs/og.js +5 -1
- package/dist/_internal/server/features/reviews/og.js +5 -1
- package/dist/_internal/server/features/seo/manifest.js +2 -1
- package/dist/_internal/server/features/seo/og.js +7 -1
- package/dist/_internal/server/features/tester/visibility.d.ts +13 -0
- package/dist/_internal/server/features/tester/visibility.js +28 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +9 -1
- package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
- package/dist/_internal/server/jobs/core/resetOtpVerification.d.ts +17 -0
- package/dist/_internal/server/jobs/core/resetOtpVerification.js +88 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +6 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +35 -0
- package/dist/_internal/server/jobs/handlers/_helpers.d.ts +6 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +12 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +2 -0
- package/dist/_internal/server/jobs/handlers/messages.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/messages.js +2 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +56 -0
- package/dist/_internal/shared/tokens/index.d.ts +12 -12
- package/dist/client.d.ts +15 -3
- package/dist/client.js +10 -2
- package/dist/constants/api-endpoints.d.ts +39 -12
- package/dist/constants/api-endpoints.js +17 -8
- package/dist/constants/field-names.d.ts +9 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.js +1 -1
- package/dist/features/account/components/LinkedAccountsSection.d.ts +11 -0
- package/dist/features/account/components/LinkedAccountsSection.js +11 -0
- package/dist/features/account/components/UserSidebar.d.ts +3 -1
- package/dist/features/account/components/UserSidebar.js +9 -13
- package/dist/features/account/hooks/useCollapsedSections.d.ts +20 -0
- package/dist/features/account/hooks/useCollapsedSections.js +63 -0
- package/dist/features/account/hooks/useProfile.d.ts +8 -0
- package/dist/features/account/index.d.ts +1 -0
- package/dist/features/account/index.js +1 -0
- package/dist/features/admin/components/AdminAddressEditorView.js +2 -2
- package/dist/features/admin/components/AdminBidsView.js +6 -6
- package/dist/features/admin/components/AdminBrandsView.js +2 -2
- package/dist/features/admin/components/AdminCarouselView.js +2 -2
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -2
- package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminProductEditorView.js +5 -2
- package/dist/features/admin/components/AdminReviewsView.js +5 -5
- package/dist/features/admin/components/AdminSectionsView.js +3 -18
- package/dist/features/admin/components/AdminSidebar.js +8 -6
- package/dist/features/admin/components/AdminSiteSettingsView.js +133 -127
- package/dist/features/admin/components/AdminSublistingCategoriesView.js +1 -1
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.d.ts +9 -0
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +129 -0
- package/dist/features/admin/components/AdminTesterChecklistView.d.ts +6 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +93 -0
- package/dist/features/admin/components/AdminUserEditorView.d.ts +3 -1
- package/dist/features/admin/components/AdminUserEditorView.js +7 -1
- package/dist/features/admin/components/AdminUsersView.js +1 -1
- package/dist/features/admin/components/DataListingView.js +2 -2
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.d.ts +8 -0
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +41 -0
- package/dist/features/admin/components/index.d.ts +6 -0
- package/dist/features/admin/components/index.js +3 -0
- package/dist/features/admin/schemas/firestore.d.ts +0 -7
- package/dist/features/admin/schemas/firestore.js +6 -6
- package/dist/features/analytics/components/PageViewTracker.d.ts +12 -0
- package/dist/features/analytics/components/PageViewTracker.js +22 -0
- package/dist/features/analytics/repository/page-views.repository.d.ts +32 -0
- package/dist/features/analytics/repository/page-views.repository.js +43 -0
- package/dist/features/analytics/types.d.ts +6 -0
- package/dist/features/analytics/types.js +13 -0
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts +12 -0
- package/dist/features/auth/hooks/useAuth.js +109 -0
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/repository/session.repository.d.ts +2 -0
- package/dist/features/auth/repository/session.repository.js +4 -1
- package/dist/features/auth/schemas/firestore.d.ts +17 -1
- package/dist/features/auth/schemas/firestore.js +2 -0
- package/dist/features/blog/components/BlogIndexListing.js +2 -2
- package/dist/features/blog/schemas/firestore.d.ts +4 -0
- package/dist/features/cart/actions/cart-actions.js +20 -2
- package/dist/features/categories/api/route.js +2 -2
- package/dist/features/categories/components/CategoriesIndexListing.js +3 -3
- package/dist/features/categories/components/CategoryBundlesListing.js +2 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +3 -1
- package/dist/features/categories/components/CategoryProductsListing.js +2 -2
- package/dist/features/categories/schemas/firestore.d.ts +4 -0
- package/dist/features/classified/components/ClassifiedIndexListing.js +2 -2
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +2 -2
- package/dist/features/email/primitives.d.ts +2 -2
- package/dist/features/email/primitives.js +2 -2
- package/dist/features/events/components/EventsIndexListing.js +2 -2
- package/dist/features/events/repository/event-entry.repository.js +1 -0
- package/dist/features/events/schemas/firestore.d.ts +7 -1
- package/dist/features/events/schemas/firestore.js +1 -0
- package/dist/features/layout/BackToTop.d.ts +6 -1
- package/dist/features/layout/BackToTop.js +31 -6
- package/dist/features/layout/BottomNavbar.js +1 -1
- package/dist/features/layout/NavItem.js +1 -1
- package/dist/features/layout/NavbarLayout.js +2 -2
- package/dist/features/live/components/LiveItemsIndexListing.js +2 -2
- package/dist/features/media/MediaVideo.js +5 -1
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -2
- package/dist/features/products/components/AuctionsIndexListing.js +2 -2
- package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -2
- package/dist/features/products/components/ProductsIndexListing.js +2 -2
- package/dist/features/products/constants/action-defs.d.ts +2 -0
- package/dist/features/products/constants/action-defs.js +2 -0
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +4 -2
- package/dist/features/products/repository/products.repository.d.ts +20 -0
- package/dist/features/products/repository/products.repository.js +5 -0
- package/dist/features/products/schemas/firestore.d.ts +4 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +18 -5
- package/dist/features/reviews/components/ReviewsIndexListing.js +2 -2
- package/dist/features/search/hooks/useNavSuggestions.js +20 -1
- package/dist/features/seller/actions/seller-actions.js +11 -6
- package/dist/features/seller/components/SellerAddressesView.js +2 -2
- package/dist/features/seller/components/SellerBidsView.js +2 -2
- package/dist/features/seller/components/SellerCouponsView.js +1 -1
- package/dist/features/seller/components/SellerOrdersView.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +2 -2
- package/dist/features/seller/components/SellerSidebar.d.ts +3 -1
- package/dist/features/seller/components/SellerSidebar.js +9 -8
- package/dist/features/seller/components/SellerStoreCategoriesView.js +2 -2
- package/dist/features/seller/components/SellerTemplatesView.js +2 -2
- package/dist/features/shell/StepForm.js +4 -4
- package/dist/features/stores/actions/store-query-actions.d.ts +5 -4
- package/dist/features/stores/actions/store-query-actions.js +11 -8
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -2
- package/dist/features/stores/components/StoreClassifiedsListing.js +2 -2
- package/dist/features/stores/components/StoreDetailLayoutView.js +1 -1
- package/dist/features/stores/components/StoreDigitalCodesListing.js +2 -2
- package/dist/features/stores/components/StoreLiveItemsListing.js +2 -2
- package/dist/features/stores/components/StorePreOrdersListing.js +2 -2
- package/dist/features/stores/components/StoreProductsListing.js +2 -2
- package/dist/features/stores/components/StoreReviewsListing.js +2 -2
- package/dist/features/stores/components/StoresIndexListing.js +2 -2
- package/dist/features/stores/repository/store.repository.js +3 -0
- package/dist/features/stores/schemas/firestore.d.ts +4 -0
- package/dist/features/tester/actions/checklist-item-actions.d.ts +69 -0
- package/dist/features/tester/actions/checklist-item-actions.js +42 -0
- package/dist/features/tester/actions/index.d.ts +1 -0
- package/dist/features/tester/actions/index.js +1 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.d.ts +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +22 -0
- package/dist/features/tester/components/AdminTesterFeedbackListView.d.ts +3 -0
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +100 -0
- package/dist/features/tester/components/AdminTesterFeedbackReportView.d.ts +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackReportView.js +30 -0
- package/dist/features/tester/components/AdminTesterFeedbackView.d.ts +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackView.js +29 -0
- package/dist/features/tester/components/TesterChecklistStepRow.d.ts +18 -0
- package/dist/features/tester/components/TesterChecklistStepRow.js +29 -0
- package/dist/features/tester/components/TesterFeedbackChart.d.ts +14 -0
- package/dist/features/tester/components/TesterFeedbackChart.js +18 -0
- package/dist/features/tester/components/TesterHubView.d.ts +5 -0
- package/dist/features/tester/components/TesterHubView.js +71 -0
- package/dist/features/tester/components/index.d.ts +11 -0
- package/dist/features/tester/components/index.js +7 -0
- package/dist/features/tester/index.d.ts +2 -0
- package/dist/features/tester/index.js +2 -0
- package/dist/features/tester/repository/index.d.ts +2 -0
- package/dist/features/tester/repository/index.js +2 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +13 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.js +55 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +50 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.js +196 -0
- package/dist/features/tester/schemas/firestore.d.ts +85 -0
- package/dist/features/tester/schemas/firestore.js +93 -0
- package/dist/features/tester/schemas/index.d.ts +1 -0
- package/dist/features/tester/schemas/index.js +1 -0
- package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/blog-tester-seed-data.js +40 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +177 -0
- package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/events-tester-seed-data.js +50 -0
- package/dist/features/tester/seed-data/index.d.ts +6 -0
- package/dist/features/tester/seed-data/index.js +6 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +151 -0
- package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +40 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +248 -0
- package/dist/features/tester/seed-data/tester-ttl.d.ts +4 -0
- package/dist/features/tester/seed-data/tester-ttl.js +6 -0
- package/dist/features/tester/server.d.ts +9 -0
- package/dist/features/tester/server.js +8 -0
- package/dist/index.d.ts +28 -2
- package/dist/index.js +51 -1
- package/dist/next/api/api-response.js +5 -1
- package/dist/next/api/routeHandler.js +10 -1
- package/dist/next/middleware/chain.js +1 -1
- package/dist/next/routing/route-map.d.ts +6 -7
- package/dist/next/routing/route-map.js +3 -7
- package/dist/providers/payment-manual/index.d.ts +2 -2
- package/dist/providers/payment-manual/index.js +2 -2
- package/dist/react/contexts/SessionContext.d.ts +4 -0
- package/dist/react/contexts/SessionContext.js +4 -0
- package/dist/repositories/index.d.ts +4 -0
- package/dist/repositories/index.js +3 -0
- package/dist/security/pii-encrypt.js +12 -2
- package/dist/seed/addresses-seed-data.js +1 -1
- package/dist/seed/bids-seed-data.js +64 -185
- package/dist/seed/blog-posts-seed-data.js +1 -1
- package/dist/seed/carousel-slides-seed-data.js +22 -16
- package/dist/seed/carousels-seed-data.js +1 -1
- package/dist/seed/cart-seed-data.js +1 -1
- package/dist/seed/categories-seed-data.js +1 -1
- package/dist/seed/claimed-coupons-seed-data.js +1 -1
- package/dist/seed/conversations-seed-data.js +1 -1
- package/dist/seed/coupon-usage-seed-data.js +1 -1
- package/dist/seed/coupons-seed-data.js +1 -1
- package/dist/seed/events-seed-data.js +1 -1
- package/dist/seed/faq-seed-data.js +119 -3
- package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
- package/dist/seed/grouped-listings-seed-data.js +1 -1
- package/dist/seed/history-seed-data.js +1 -1
- package/dist/seed/homepage-sections-seed-data.js +1 -1
- package/dist/seed/index.d.ts +2 -2
- package/dist/seed/index.js +7 -3
- package/dist/seed/lottery-entries-seed-data.js +1 -1
- package/dist/seed/manifest.d.ts +4 -3
- package/dist/seed/manifest.js +13 -6
- package/dist/seed/notifications-seed-data.js +1 -1
- package/dist/seed/offers-seed-data.js +1 -1
- package/dist/seed/orders-seed-data.js +1 -1
- package/dist/seed/payouts-seed-data.js +1 -1
- package/dist/seed/products-art-seed-data.js +1 -1
- package/dist/seed/products-auctions-seed-data.js +1 -1
- package/dist/seed/products-classifieds-seed-data.js +1 -1
- package/dist/seed/products-digital-codes-seed-data.js +1 -1
- package/dist/seed/products-live-items-seed-data.js +1 -1
- package/dist/seed/products-preorders-seed-data.d.ts +2 -0
- package/dist/seed/products-preorders-seed-data.js +1 -1
- package/dist/seed/products-prize-draws-seed-data.js +1 -1
- package/dist/seed/products-standard-seed-data.js +1 -1
- package/dist/seed/products-stickers-seed-data.js +1 -1
- package/dist/seed/reviews-seed-data.js +1 -1
- package/dist/seed/scammers-seed-data.js +1 -1
- package/dist/seed/sessions-seed-data.js +1 -1
- package/dist/seed/site-settings-seed-data.js +22 -16
- package/dist/seed/store-addresses-seed-data.js +1 -1
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/seed/stores-seed-data.js +1 -1
- package/dist/seed/support-tickets-seed-data.js +1 -1
- package/dist/seed/tester-checklist-seed-data.d.ts +2 -0
- package/dist/seed/tester-checklist-seed-data.js +248 -0
- package/dist/seed/users-seed-data.js +1 -1
- package/dist/seed/wishlists-seed-data.js +1 -1
- package/dist/server.d.ts +7 -2
- package/dist/server.js +19 -12
- package/dist/styles.css +181 -165
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/color-pairs.d.ts +46 -0
- package/dist/tokens/color-pairs.js +93 -0
- package/dist/tokens/index.d.ts +60 -60
- package/dist/tokens/index.js +30 -30
- package/dist/tokens/themes/cobalt-night.d.ts +3 -2
- package/dist/tokens/themes/cobalt-night.js +3 -2
- package/dist/tokens/themes/default-dark.d.ts +4 -1
- package/dist/tokens/themes/default-dark.js +53 -50
- package/dist/tokens/themes/default-light.d.ts +5 -1
- package/dist/tokens/themes/default-light.js +48 -44
- package/dist/tokens/themes/sunset.js +4 -4
- package/dist/tokens/tokens.css +128 -122
- package/dist/ui/components/CollapsibleSection.d.ts +15 -0
- package/dist/ui/components/CollapsibleSection.js +13 -0
- package/dist/ui/components/ListingToolbar.js +3 -2
- package/dist/ui/components/Semantic.style.css +3 -3
- package/dist/ui/components/SideDrawer.style.css +19 -19
- package/dist/ui/components/Slider.js +1 -1
- package/dist/ui/components/StickyToolbar.d.ts +36 -7
- package/dist/ui/components/StickyToolbar.js +63 -5
- package/dist/ui/components/Tabs.js +17 -1
- package/dist/ui/components/Tabs.style.css +10 -0
- package/dist/ui/components/Typography.js +7 -0
- package/dist/ui/components/surface-tokens.d.ts +0 -7
- package/dist/ui/components/surface-tokens.js +7 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.style.css +20 -20
- package/dist/utils/id-generators.d.ts +13 -0
- package/dist/utils/id-generators.js +17 -0
- package/package.json +1 -1
- package/scripts/seed-cli-loader.mjs +15 -0
- package/scripts/seed-cli.mjs +71 -17
|
@@ -4,7 +4,7 @@ import { Row, SIEVE_OP, Stack, sieveFilter } from "@mohasinac/appkit";
|
|
|
4
4
|
import { useState, useCallback, useMemo } from "react";
|
|
5
5
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
6
|
import { useStores } from "../hooks/useStores";
|
|
7
|
-
import { BulkActionBar, Div, ListingFilterDrawer, ListingToolbar, Pagination, Text } from "../../../ui";
|
|
7
|
+
import { BulkActionBar, Div, ListingFilterDrawer, ListingToolbar, Pagination, Text, StickyToolbar } from "../../../ui";
|
|
8
8
|
import { ROUTES } from "../../../next";
|
|
9
9
|
import { InteractiveStoreCard } from "./InteractiveStoreCard";
|
|
10
10
|
import { StoreFilters } from "./StoreFilters";
|
|
@@ -161,7 +161,7 @@ export function StoresIndexListing({ initialData }) {
|
|
|
161
161
|
disabled: selection.selectedCount !== 1,
|
|
162
162
|
onClick: () => { selection.clearSelection(); },
|
|
163
163
|
},
|
|
164
|
-
] }), totalPages > 1 && (_jsx(
|
|
164
|
+
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: table.getNumber("page", 1), totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Div, { className: `${__P.p4} space-y-[0.625rem]`, children: [_jsx(Row, { align: "center", gap: "sm", children: _jsx(Div, { className: "h-10 w-10", surface: "subtle", rounded: "lg" }) }), _jsx(Div, { className: "h-4 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" })] })] }, i))) })) : stores.length === 0 ? (_jsx(Text, { paddingY: "4xl", color: "muted", size: "sm", align: "start", children: "No stores found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: stores.map((store) => {
|
|
165
165
|
const storeKey = store.storeSlug ?? store.id;
|
|
166
166
|
return (_jsx(InteractiveStoreCard, { store: store, href: String(ROUTES.PUBLIC.STORE_DETAIL(storeKey)), selectable: selection.isSelecting, isSelected: selection.isSelected(store.id ?? store.storeSlug), onSelect: (id, sel) => { void sel; selection.toggle(id); } }, storeKey));
|
|
167
167
|
}) })) : (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: stores.map((store) => {
|
|
@@ -119,6 +119,9 @@ export class StoreRepository extends BaseRepository {
|
|
|
119
119
|
[STORE_FIELDS.STATUS]: { canFilter: true, canSort: false },
|
|
120
120
|
[STORE_FIELDS.IS_PUBLIC]: { canFilter: true, canSort: false },
|
|
121
121
|
[STORE_FIELDS.CREATED_AT]: { canFilter: false, canSort: true },
|
|
122
|
+
"stats.itemsSold": { canFilter: false, canSort: true },
|
|
123
|
+
"stats.averageRating": { canFilter: false, canSort: true },
|
|
124
|
+
"stats.totalProducts": { canFilter: true, canSort: false },
|
|
122
125
|
};
|
|
123
126
|
let baseQuery = this.getCollection();
|
|
124
127
|
if (activeOnly) {
|
|
@@ -50,6 +50,10 @@ export type StoreStatus = (typeof StoreStatusValues)[keyof typeof StoreStatusVal
|
|
|
50
50
|
export interface StoreDocument extends BaseDocument {
|
|
51
51
|
storeSlug: string;
|
|
52
52
|
ownerId: string;
|
|
53
|
+
/** Tester sandbox flag — set on the shared admin-seeded test store; swept by testerSandboxCleanup. */
|
|
54
|
+
isTestData?: boolean;
|
|
55
|
+
/** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
|
|
56
|
+
testDataExpiresAt?: Date;
|
|
53
57
|
storeName: string;
|
|
54
58
|
storeDescription?: string;
|
|
55
59
|
storeCategory?: string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { TesterChecklistItemDocument } from "../schemas/firestore";
|
|
3
|
+
export declare const checklistItemCreateSchema: z.ZodObject<{
|
|
4
|
+
groupKey: z.ZodString;
|
|
5
|
+
groupLabel: z.ZodString;
|
|
6
|
+
pageKey: z.ZodString;
|
|
7
|
+
pageLabel: z.ZodString;
|
|
8
|
+
label: z.ZodString;
|
|
9
|
+
description: z.ZodOptional<z.ZodString>;
|
|
10
|
+
href: z.ZodOptional<z.ZodString>;
|
|
11
|
+
order: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
isActive: z.ZodDefault<z.ZodBoolean>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
label: string;
|
|
15
|
+
order: number;
|
|
16
|
+
isActive: boolean;
|
|
17
|
+
groupKey: string;
|
|
18
|
+
pageKey: string;
|
|
19
|
+
groupLabel: string;
|
|
20
|
+
pageLabel: string;
|
|
21
|
+
href?: string | undefined;
|
|
22
|
+
description?: string | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
label: string;
|
|
25
|
+
groupKey: string;
|
|
26
|
+
pageKey: string;
|
|
27
|
+
groupLabel: string;
|
|
28
|
+
pageLabel: string;
|
|
29
|
+
href?: string | undefined;
|
|
30
|
+
description?: string | undefined;
|
|
31
|
+
order?: number | undefined;
|
|
32
|
+
isActive?: boolean | undefined;
|
|
33
|
+
}>;
|
|
34
|
+
export declare const checklistItemUpdateSchema: z.ZodObject<{
|
|
35
|
+
groupKey: z.ZodOptional<z.ZodString>;
|
|
36
|
+
groupLabel: z.ZodOptional<z.ZodString>;
|
|
37
|
+
pageKey: z.ZodOptional<z.ZodString>;
|
|
38
|
+
pageLabel: z.ZodOptional<z.ZodString>;
|
|
39
|
+
label: z.ZodOptional<z.ZodString>;
|
|
40
|
+
description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
41
|
+
href: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
42
|
+
order: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
43
|
+
isActive: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
44
|
+
}, "strip", z.ZodTypeAny, {
|
|
45
|
+
label?: string | undefined;
|
|
46
|
+
href?: string | undefined;
|
|
47
|
+
description?: string | undefined;
|
|
48
|
+
order?: number | undefined;
|
|
49
|
+
isActive?: boolean | undefined;
|
|
50
|
+
groupKey?: string | undefined;
|
|
51
|
+
pageKey?: string | undefined;
|
|
52
|
+
groupLabel?: string | undefined;
|
|
53
|
+
pageLabel?: string | undefined;
|
|
54
|
+
}, {
|
|
55
|
+
label?: string | undefined;
|
|
56
|
+
href?: string | undefined;
|
|
57
|
+
description?: string | undefined;
|
|
58
|
+
order?: number | undefined;
|
|
59
|
+
isActive?: boolean | undefined;
|
|
60
|
+
groupKey?: string | undefined;
|
|
61
|
+
pageKey?: string | undefined;
|
|
62
|
+
groupLabel?: string | undefined;
|
|
63
|
+
pageLabel?: string | undefined;
|
|
64
|
+
}>;
|
|
65
|
+
export type ChecklistItemCreateInput = z.infer<typeof checklistItemCreateSchema>;
|
|
66
|
+
export type ChecklistItemUpdateInput = z.infer<typeof checklistItemUpdateSchema>;
|
|
67
|
+
export declare function createChecklistItem(input: ChecklistItemCreateInput): Promise<TesterChecklistItemDocument>;
|
|
68
|
+
export declare function updateChecklistItem(id: string, input: ChecklistItemUpdateInput): Promise<TesterChecklistItemDocument>;
|
|
69
|
+
export declare function deleteChecklistItem(id: string): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { serverLogger } from "../../../monitoring";
|
|
3
|
+
import { testerChecklistItemRepository } from "../repository/tester-checklist-item.repository";
|
|
4
|
+
// --- Schemas --------------------------------------------------------------
|
|
5
|
+
const checklistItemBaseSchema = z.object({
|
|
6
|
+
groupKey: z.string().min(1).max(100),
|
|
7
|
+
groupLabel: z.string().min(1).max(200),
|
|
8
|
+
pageKey: z.string().min(1).max(100),
|
|
9
|
+
pageLabel: z.string().min(1).max(200),
|
|
10
|
+
label: z.string().min(3).max(500),
|
|
11
|
+
description: z.string().max(2000).optional(),
|
|
12
|
+
href: z.string().max(300).optional(),
|
|
13
|
+
order: z.number().int().min(0).default(0),
|
|
14
|
+
isActive: z.boolean().default(true),
|
|
15
|
+
});
|
|
16
|
+
export const checklistItemCreateSchema = checklistItemBaseSchema;
|
|
17
|
+
export const checklistItemUpdateSchema = checklistItemBaseSchema.partial();
|
|
18
|
+
// --- Actions --------------------------------------------------------------
|
|
19
|
+
export async function createChecklistItem(input) {
|
|
20
|
+
const item = await testerChecklistItemRepository.createItem({
|
|
21
|
+
groupKey: input.groupKey,
|
|
22
|
+
groupLabel: input.groupLabel,
|
|
23
|
+
pageKey: input.pageKey,
|
|
24
|
+
pageLabel: input.pageLabel,
|
|
25
|
+
label: input.label,
|
|
26
|
+
description: input.description,
|
|
27
|
+
href: input.href,
|
|
28
|
+
order: input.order ?? 0,
|
|
29
|
+
isActive: input.isActive ?? true,
|
|
30
|
+
});
|
|
31
|
+
serverLogger.info("createChecklistItem", { itemId: item.id });
|
|
32
|
+
return item;
|
|
33
|
+
}
|
|
34
|
+
export async function updateChecklistItem(id, input) {
|
|
35
|
+
const item = await testerChecklistItemRepository.update(id, input);
|
|
36
|
+
serverLogger.info("updateChecklistItem", { itemId: id });
|
|
37
|
+
return item;
|
|
38
|
+
}
|
|
39
|
+
export async function deleteChecklistItem(id) {
|
|
40
|
+
await testerChecklistItemRepository.delete(id);
|
|
41
|
+
serverLogger.info("deleteChecklistItem", { itemId: id });
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./checklist-item-actions";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./checklist-item-actions";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { Card, Stack, Text } from "../../../ui";
|
|
5
|
+
import { MediaImage } from "../../media/MediaImage";
|
|
6
|
+
import { apiClient } from "../../../http";
|
|
7
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
|
+
export function AdminTesterFeedbackIssuesView() {
|
|
9
|
+
const query = useQuery({
|
|
10
|
+
queryKey: ["admin", "tester-feedback", "report"],
|
|
11
|
+
queryFn: async () => {
|
|
12
|
+
const res = await apiClient.get(ADMIN_ENDPOINTS.TESTER_FEEDBACK_REPORT);
|
|
13
|
+
return res?.data ?? res;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const issues = query.data?.issues ?? [];
|
|
17
|
+
if (query.isLoading)
|
|
18
|
+
return _jsx(Text, { color: "muted", children: "Loading issues\u2026" });
|
|
19
|
+
if (issues.length === 0)
|
|
20
|
+
return _jsx(Text, { color: "muted", children: "No \"No\" answers recorded yet." });
|
|
21
|
+
return (_jsx(Stack, { gap: "sm", children: issues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { weight: "semibold", children: [issue.groupKey, " / ", issue.pageKey] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" }))] }) }, issue.id))) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useApiMutation } from "../../../client";
|
|
4
|
+
import { SIEVE_OP, sieveFilter } from "../../../utils/sieve-builder";
|
|
5
|
+
import { sortBy } from "../../../constants/sort";
|
|
6
|
+
import { TESTER_CHECKLIST_RESPONSE_FIELDS } from "../schemas/firestore";
|
|
7
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { FilterChipGroup, RowActionMenu, Stack, Text } from "../../../ui";
|
|
9
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
11
|
+
import { toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks/useAdminListingData";
|
|
12
|
+
import { DataListingView } from "../../admin/components/DataListingView";
|
|
13
|
+
import { apiClient } from "../../../http";
|
|
14
|
+
const COLUMNS = [
|
|
15
|
+
{
|
|
16
|
+
key: "primary",
|
|
17
|
+
header: "Tester / Case",
|
|
18
|
+
sortable: true,
|
|
19
|
+
render: (row) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", children: row.secondary })] })),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "answer",
|
|
23
|
+
header: "Answer",
|
|
24
|
+
className: "w-24",
|
|
25
|
+
render: (row) => (_jsx(Text, { weight: "medium", color: row.answer === "no" ? "error" : "success", children: row.answer === "no" ? "No" : row.answer === "yes" ? "Yes" : "—" })),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "status",
|
|
29
|
+
header: "Status",
|
|
30
|
+
className: "w-28",
|
|
31
|
+
render: (row) => _jsx(Text, { size: "sm", color: "muted", children: row.status }),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "updatedAt",
|
|
35
|
+
header: "Updated",
|
|
36
|
+
className: "w-32",
|
|
37
|
+
render: (row) => _jsx(Text, { size: "sm", color: "muted", children: row.updatedAt }),
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
export function AdminTesterFeedbackListView(_props) {
|
|
41
|
+
const queryClient = useQueryClient();
|
|
42
|
+
const patchMutation = useApiMutation({
|
|
43
|
+
mutationFn: async (id) => {
|
|
44
|
+
await apiClient.patch(ADMIN_ENDPOINTS.TESTER_FEEDBACK_BY_ID(id), {});
|
|
45
|
+
},
|
|
46
|
+
onSuccess: () => {
|
|
47
|
+
queryClient.invalidateQueries({ queryKey: ["admin", "tester-feedback"] });
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const config = {
|
|
51
|
+
portal: "admin",
|
|
52
|
+
title: "All Submissions",
|
|
53
|
+
searchPlaceholder: "Search tester, comment, or case",
|
|
54
|
+
emptyLabel: "No feedback submitted yet",
|
|
55
|
+
filterKeys: ["answer", "status"],
|
|
56
|
+
defaultSort: sortBy(TESTER_CHECKLIST_RESPONSE_FIELDS.CREATED_AT, "DESC"),
|
|
57
|
+
queryKey: ["admin", "tester-feedback", "listing"],
|
|
58
|
+
endpoint: ADMIN_ENDPOINTS.TESTER_FEEDBACK,
|
|
59
|
+
sortOptions: [
|
|
60
|
+
{ value: sortBy(TESTER_CHECKLIST_RESPONSE_FIELDS.CREATED_AT, "DESC"), label: "Newest" },
|
|
61
|
+
{ value: sortBy(TESTER_CHECKLIST_RESPONSE_FIELDS.CREATED_AT, "ASC"), label: "Oldest" },
|
|
62
|
+
],
|
|
63
|
+
columns: COLUMNS,
|
|
64
|
+
mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
|
|
65
|
+
id: toStringValue(item.id, `feedback-${index}`),
|
|
66
|
+
primary: toStringValue(item.testerDisplayName, "Unknown tester"),
|
|
67
|
+
secondary: `${toStringValue(item.groupKey, "")} / ${toStringValue(item.pageKey, "")}${item.comment ? " — " + toStringValue(item.comment, "") : ""}`,
|
|
68
|
+
answer: toStringValue(item.answer, ""),
|
|
69
|
+
status: toStringValue(item.status, "new"),
|
|
70
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
71
|
+
screenshotUrl: item.screenshotUrl ? toStringValue(item.screenshotUrl, "") : undefined,
|
|
72
|
+
})),
|
|
73
|
+
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
74
|
+
buildFilters: (state) => {
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (state.answer && state.answer !== "All")
|
|
77
|
+
parts.push(sieveFilter("answer", SIEVE_OP.EQ, state.answer));
|
|
78
|
+
if (state.status && state.status !== "All")
|
|
79
|
+
parts.push(sieveFilter("status", SIEVE_OP.EQ, state.status));
|
|
80
|
+
return parts.join(",") || undefined;
|
|
81
|
+
},
|
|
82
|
+
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Answer", tabs: [
|
|
83
|
+
{ id: "All", label: "All" },
|
|
84
|
+
{ id: "yes", label: "Yes" },
|
|
85
|
+
{ id: "no", label: "No" },
|
|
86
|
+
], value: pendingFilters.answer || "", onChange: (v) => setPendingFilters((p) => ({ ...p, answer: v })) }), _jsx(FilterChipGroup, { label: "Status", tabs: [
|
|
87
|
+
{ id: "All", label: "All" },
|
|
88
|
+
{ id: "new", label: "New" },
|
|
89
|
+
{ id: "reviewed", label: "Reviewed" },
|
|
90
|
+
], value: pendingFilters.status || "", onChange: (v) => setPendingFilters((p) => ({ ...p, status: v })) })] })),
|
|
91
|
+
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
92
|
+
{
|
|
93
|
+
label: ACTIONS.ADMIN["mark-feedback-reviewed"].label,
|
|
94
|
+
disabled: row.status === "reviewed",
|
|
95
|
+
onClick: () => patchMutation.mutate(row.id),
|
|
96
|
+
},
|
|
97
|
+
] })),
|
|
98
|
+
};
|
|
99
|
+
return _jsx(DataListingView, { config: config });
|
|
100
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { Stack, Text } from "../../../ui";
|
|
5
|
+
import { apiClient } from "../../../http";
|
|
6
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
|
+
import { TesterFeedbackChart } from "./TesterFeedbackChart";
|
|
8
|
+
export function AdminTesterFeedbackReportView() {
|
|
9
|
+
const query = useQuery({
|
|
10
|
+
queryKey: ["admin", "tester-feedback", "report"],
|
|
11
|
+
queryFn: async () => {
|
|
12
|
+
const res = await apiClient.get(ADMIN_ENDPOINTS.TESTER_FEEDBACK_REPORT);
|
|
13
|
+
return res?.data ?? res;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
if (query.isLoading)
|
|
17
|
+
return _jsx(Text, { color: "muted", children: "Loading report\u2026" });
|
|
18
|
+
const itemCoverage = query.data?.itemCoverage ?? [];
|
|
19
|
+
const totals = query.data?.totals ?? { totalAnswered: 0, totalYes: 0, totalNo: 0 };
|
|
20
|
+
const byGroup = new Map();
|
|
21
|
+
for (const item of itemCoverage) {
|
|
22
|
+
if (!byGroup.has(item.groupKey)) {
|
|
23
|
+
byGroup.set(item.groupKey, { groupLabel: item.groupKey, yes: 0, no: 0 });
|
|
24
|
+
}
|
|
25
|
+
const entry = byGroup.get(item.groupKey);
|
|
26
|
+
entry.yes += item.yesCount;
|
|
27
|
+
entry.no += item.noCount;
|
|
28
|
+
}
|
|
29
|
+
return (_jsx(Stack, { gap: "lg", children: _jsx(TesterFeedbackChart, { data: Array.from(byGroup.values()), totalCases: itemCoverage.length, totalAnswered: totals.totalAnswered, totalYes: totals.totalYes, totalNo: totals.totalNo }) }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button, Heading, Row, Stack, Tabs, TabsContent, TabsList, TabsTrigger, useToast } from "../../../ui";
|
|
4
|
+
import { apiClient } from "../../../http";
|
|
5
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
6
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
7
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
|
+
import { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
|
|
9
|
+
import { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
|
|
10
|
+
import { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
|
|
11
|
+
export function AdminTesterFeedbackView() {
|
|
12
|
+
const { showToast } = useToast();
|
|
13
|
+
const handleExportReport = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const blob = await apiClient.blob(ADMIN_ENDPOINTS.TESTER_FEEDBACK_EXPORT);
|
|
16
|
+
const url = URL.createObjectURL(blob);
|
|
17
|
+
const a = document.createElement("a");
|
|
18
|
+
a.href = url;
|
|
19
|
+
a.download = `tester-feedback-report-${new Date().toISOString().slice(0, 10)}.md`;
|
|
20
|
+
a.click();
|
|
21
|
+
URL.revokeObjectURL(url);
|
|
22
|
+
}
|
|
23
|
+
catch (_err) {
|
|
24
|
+
void normalizeError(_err);
|
|
25
|
+
showToast("Report export failed.", "error");
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return (_jsxs(Stack, { gap: "lg", children: [_jsxs(Row, { justify: "between", align: "center", wrap: true, children: [_jsx(Heading, { level: 1, children: "Tester Feedback" }), _jsx(Button, { variant: "ghost", action: ACTIONS.ADMIN["export-tester-feedback"], onClick: handleExportReport })] }), _jsxs(Tabs, { defaultValue: "report", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "report", children: "Report" }), _jsx(TabsTrigger, { value: "issues", children: "Main Issues" }), _jsx(TabsTrigger, { value: "submissions", children: "All Submissions" })] }), _jsx(TabsContent, { value: "report", children: _jsx(AdminTesterFeedbackReportView, {}) }), _jsx(TabsContent, { value: "issues", children: _jsx(AdminTesterFeedbackIssuesView, {}) }), _jsx(TabsContent, { value: "submissions", children: _jsx(AdminTesterFeedbackListView, {}) })] })] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { TesterAnswer } from "../schemas/firestore";
|
|
3
|
+
export interface TesterChecklistStepItem {
|
|
4
|
+
checklistItemId: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
href?: string;
|
|
8
|
+
answer: TesterAnswer | null;
|
|
9
|
+
comment?: string;
|
|
10
|
+
screenshotUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TesterChecklistStepRowProps {
|
|
13
|
+
item: TesterChecklistStepItem;
|
|
14
|
+
testerDisplayName: string;
|
|
15
|
+
onAnswer: (checklistItemId: string, answer: TesterAnswer) => void;
|
|
16
|
+
onSaveNote: (checklistItemId: string, comment: string, screenshotUrl: string) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function TesterChecklistStepRow({ item, testerDisplayName, onAnswer, onSaveNote, }: TesterChecklistStepRowProps): React.JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Anchor, Button, Div, FieldTextarea, Row, Stack, Text } from "../../../ui";
|
|
5
|
+
import { MediaUploadField } from "../../media/upload/MediaUploadField";
|
|
6
|
+
import { useMediaUpload } from "../../media/hooks/useMedia";
|
|
7
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
8
|
+
export function TesterChecklistStepRow({ item, testerDisplayName, onAnswer, onSaveNote, }) {
|
|
9
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
10
|
+
const [comment, setComment] = React.useState(item.comment ?? "");
|
|
11
|
+
const [screenshotUrl, setScreenshotUrl] = React.useState(item.screenshotUrl ?? "");
|
|
12
|
+
const [saving, setSaving] = React.useState(false);
|
|
13
|
+
const { upload } = useMediaUpload();
|
|
14
|
+
const handleUpload = async (file) => upload(file, "tester-screenshots", false, {
|
|
15
|
+
type: "tester-screenshot",
|
|
16
|
+
testerName: testerDisplayName,
|
|
17
|
+
checklistItemId: item.checklistItemId,
|
|
18
|
+
});
|
|
19
|
+
const handleSave = async () => {
|
|
20
|
+
setSaving(true);
|
|
21
|
+
try {
|
|
22
|
+
await onSaveNote(item.checklistItemId, comment, screenshotUrl);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
setSaving(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return (_jsxs(Stack, { gap: "xs", padding: "sm", border: "default", rounded: "md", children: [_jsxs(Row, { gap: "sm", justify: "between", align: "center", wrap: true, children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "medium", children: item.label }), item.description && (_jsx(Text, { size: "xs", color: "muted", children: item.description })), item.href && (_jsx(Anchor, { href: item.href, tone: "brand", underline: "hover", children: "Go test this \u2192" }))] }), _jsxs(Row, { gap: "xs", children: [_jsx(Button, { type: "button", size: "sm", variant: item.answer === "yes" ? "primary" : "outline", onClick: () => onAnswer(item.checklistItemId, "yes"), children: "Yes" }), _jsx(Button, { type: "button", size: "sm", variant: item.answer === "no" ? "danger" : "outline", onClick: () => onAnswer(item.checklistItemId, "no"), children: "No" }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => setExpanded((v) => !v), children: expanded ? "Hide note" : "Add note" })] })] }), expanded && (_jsx(Div, { padding: "t-sm", border: "default", className: "border-t", children: _jsxs(Stack, { gap: "sm", children: [_jsx(FieldTextarea, { name: `comment-${item.checklistItemId}`, label: "Comment", value: comment, onChange: setComment, placeholder: "What did you notice? Colors, styles, readability, bugs..." }), _jsx(MediaUploadField, { label: "Screenshot (optional)", value: screenshotUrl, onChange: setScreenshotUrl, onUpload: handleUpload, kind: "image" }), _jsx(Row, { children: _jsx(Button, { type: "button", action: ACTIONS.TESTER["save-note"], isLoading: saving, onClick: handleSave }) })] }) }))] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface GroupCoverageDatum {
|
|
3
|
+
groupLabel: string;
|
|
4
|
+
yes: number;
|
|
5
|
+
no: number;
|
|
6
|
+
}
|
|
7
|
+
export interface TesterFeedbackChartProps {
|
|
8
|
+
data: GroupCoverageDatum[];
|
|
9
|
+
totalCases: number;
|
|
10
|
+
totalAnswered: number;
|
|
11
|
+
totalYes: number;
|
|
12
|
+
totalNo: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function TesterFeedbackChart({ data, totalCases, totalAnswered, totalYes, totalNo, }: TesterFeedbackChartProps): React.JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
4
|
+
import { Card, Heading, Row, Stack, Text } from "../../../ui";
|
|
5
|
+
// recharts exports generic components (<DataPointType>) that don't satisfy
|
|
6
|
+
// next/dynamic's ComponentType<P> constraint — cast each to ComponentType<any>.
|
|
7
|
+
const ResponsiveContainer = dynamic(() => import("recharts").then((m) => m.ResponsiveContainer), { ssr: false });
|
|
8
|
+
const BarChart = dynamic(() => import("recharts").then((m) => m.BarChart), { ssr: false });
|
|
9
|
+
const Bar = dynamic(() => import("recharts").then((m) => m.Bar), { ssr: false });
|
|
10
|
+
const XAxis = dynamic(() => import("recharts").then((m) => m.XAxis), { ssr: false });
|
|
11
|
+
const YAxis = dynamic(() => import("recharts").then((m) => m.YAxis), { ssr: false });
|
|
12
|
+
const CartesianGrid = dynamic(() => import("recharts").then((m) => m.CartesianGrid), { ssr: false });
|
|
13
|
+
const Tooltip = dynamic(() => import("recharts").then((m) => m.Tooltip), { ssr: false });
|
|
14
|
+
const Legend = dynamic(() => import("recharts").then((m) => m.Legend), { ssr: false });
|
|
15
|
+
export function TesterFeedbackChart({ data, totalCases, totalAnswered, totalYes, totalNo, }) {
|
|
16
|
+
const passRate = totalAnswered > 0 ? Math.round((totalYes / totalAnswered) * 100) : 0;
|
|
17
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Card, { padding: "md", variant: "default", children: [_jsx(Text, { size: "xs", color: "muted", children: "Total test cases" }), _jsx(Heading, { level: 3, children: totalCases })] }), _jsxs(Card, { padding: "md", variant: "default", children: [_jsx(Text, { size: "xs", color: "muted", children: "Answers recorded" }), _jsx(Heading, { level: 3, children: totalAnswered })] }), _jsxs(Card, { padding: "md", variant: "default", children: [_jsx(Text, { size: "xs", color: "muted", children: "Pass rate" }), _jsxs(Heading, { level: 3, children: [passRate, "%"] })] }), _jsxs(Card, { padding: "md", variant: "default", children: [_jsx(Text, { size: "xs", color: "muted", children: "Issues (No)" }), _jsx(Heading, { level: 3, color: "error", children: totalNo })] })] }), _jsx(ResponsiveContainer, { width: "100%", height: 360, children: _jsxs(BarChart, { data: data, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "groupLabel", tick: { fontSize: 12 } }), _jsx(YAxis, { allowDecimals: false }), _jsx(Tooltip, {}), _jsx(Legend, {}), _jsx(Bar, { dataKey: "yes", name: "Yes", fill: "var(--appkit-color-success)" }), _jsx(Bar, { dataKey: "no", name: "No", fill: "var(--appkit-color-error)" })] }) })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { Alert, Details, Div, FieldInput, Heading, Stack, Summary, Text } from "../../../ui";
|
|
6
|
+
import { apiClient } from "../../../http";
|
|
7
|
+
import { useApiMutation } from "../../../client";
|
|
8
|
+
import { useSession } from "../../../react";
|
|
9
|
+
import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
|
+
import { TesterChecklistStepRow } from "./TesterChecklistStepRow";
|
|
11
|
+
function matchesQuery(item, query) {
|
|
12
|
+
if (!query)
|
|
13
|
+
return true;
|
|
14
|
+
const haystack = `${item.label} ${item.description ?? ""} ${item.groupLabel} ${item.pageLabel}`.toLowerCase();
|
|
15
|
+
return haystack.includes(query.toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
export function TesterHubView({ sandboxExpiresAt }) {
|
|
18
|
+
const { user } = useSession();
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
const [search, setSearch] = React.useState("");
|
|
21
|
+
const query = useQuery({
|
|
22
|
+
queryKey: ["user", "tester-checklist"],
|
|
23
|
+
queryFn: async () => {
|
|
24
|
+
const res = await apiClient.get(ACCOUNT_ENDPOINTS.TESTER_CHECKLIST);
|
|
25
|
+
return res?.data ?? res;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const upsertMutation = useApiMutation({
|
|
29
|
+
mutationFn: async (vars) => apiClient.put(ACCOUNT_ENDPOINTS.TESTER_CHECKLIST_ITEM_BY_ID(vars.checklistItemId), vars.patch),
|
|
30
|
+
onSuccess: () => {
|
|
31
|
+
void queryClient.invalidateQueries({ queryKey: ["user", "tester-checklist"] });
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (!user?.isTester) {
|
|
35
|
+
return (_jsx(Alert, { variant: "warning", title: "Testers only", children: "This page is only available to accounts flagged as testers. Contact an admin if you believe this is a mistake." }));
|
|
36
|
+
}
|
|
37
|
+
const items = query.data?.items ?? [];
|
|
38
|
+
const filtered = items.filter((item) => matchesQuery(item, search));
|
|
39
|
+
const groups = new Map();
|
|
40
|
+
for (const item of filtered) {
|
|
41
|
+
if (!groups.has(item.groupKey)) {
|
|
42
|
+
groups.set(item.groupKey, { groupLabel: item.groupLabel, pages: new Map() });
|
|
43
|
+
}
|
|
44
|
+
const group = groups.get(item.groupKey);
|
|
45
|
+
if (!group.pages.has(item.pageKey)) {
|
|
46
|
+
group.pages.set(item.pageKey, { pageLabel: item.pageLabel, items: [] });
|
|
47
|
+
}
|
|
48
|
+
group.pages.get(item.pageKey).items.push(item);
|
|
49
|
+
}
|
|
50
|
+
const handleAnswer = (checklistItemId, answer) => {
|
|
51
|
+
upsertMutation.mutate({ checklistItemId, patch: { answer } });
|
|
52
|
+
};
|
|
53
|
+
const handleSaveNote = async (checklistItemId, comment, screenshotUrl) => {
|
|
54
|
+
await upsertMutation.mutateAsync({ checklistItemId, patch: { comment, screenshotUrl } });
|
|
55
|
+
};
|
|
56
|
+
return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "e.g. checkout, google, bidding..." }), query.isLoading && _jsx(Text, { color: "muted", children: "Loading checklist\u2026" }), Array.from(groups.entries()).map(([groupKey, group]) => (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, children: group.groupLabel }), Array.from(group.pages.entries()).map(([pageKey, page]) => {
|
|
57
|
+
const answeredCount = page.items.filter((i) => i.answer).length;
|
|
58
|
+
return (_jsxs(Details, { tone: "card", defaultOpen: false, children: [_jsxs(Summary, { children: [page.pageLabel, " (", answeredCount, "/", page.items.length, " answered)"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: page.items
|
|
59
|
+
.slice()
|
|
60
|
+
.sort((a, b) => a.order - b.order)
|
|
61
|
+
.map((item) => (_jsx(TesterChecklistStepRow, { testerDisplayName: user?.displayName ?? user?.email ?? "tester", item: {
|
|
62
|
+
checklistItemId: item.id,
|
|
63
|
+
label: item.label,
|
|
64
|
+
description: item.description,
|
|
65
|
+
href: item.href,
|
|
66
|
+
answer: item.answer,
|
|
67
|
+
comment: item.comment,
|
|
68
|
+
screenshotUrl: item.screenshotUrl,
|
|
69
|
+
}, onAnswer: handleAnswer, onSaveNote: handleSaveNote }, item.id))) }) })] }, pageKey));
|
|
70
|
+
})] }, groupKey)))] }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { TesterHubView } from "./TesterHubView";
|
|
2
|
+
export type { TesterHubViewProps } from "./TesterHubView";
|
|
3
|
+
export { TesterChecklistStepRow } from "./TesterChecklistStepRow";
|
|
4
|
+
export type { TesterChecklistStepRowProps, TesterChecklistStepItem } from "./TesterChecklistStepRow";
|
|
5
|
+
export { TesterFeedbackChart } from "./TesterFeedbackChart";
|
|
6
|
+
export type { TesterFeedbackChartProps, GroupCoverageDatum } from "./TesterFeedbackChart";
|
|
7
|
+
export { AdminTesterFeedbackView } from "./AdminTesterFeedbackView";
|
|
8
|
+
export { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
|
|
9
|
+
export { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
|
|
10
|
+
export { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
|
|
11
|
+
export type { AdminTesterFeedbackListViewProps } from "./AdminTesterFeedbackListView";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { TesterHubView } from "./TesterHubView";
|
|
2
|
+
export { TesterChecklistStepRow } from "./TesterChecklistStepRow";
|
|
3
|
+
export { TesterFeedbackChart } from "./TesterFeedbackChart";
|
|
4
|
+
export { AdminTesterFeedbackView } from "./AdminTesterFeedbackView";
|
|
5
|
+
export { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
|
|
6
|
+
export { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
|
|
7
|
+
export { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
|
+
import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
|
|
3
|
+
import { type TesterChecklistItemDocument, type TesterChecklistItemCreateInput, type TesterChecklistItemUpdateInput } from "../schemas/firestore";
|
|
4
|
+
export declare class TesterChecklistItemRepository extends BaseRepository<TesterChecklistItemDocument> {
|
|
5
|
+
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
6
|
+
constructor();
|
|
7
|
+
private buildSearchTokens;
|
|
8
|
+
createItem(input: TesterChecklistItemCreateInput): Promise<TesterChecklistItemDocument>;
|
|
9
|
+
update(id: string, data: TesterChecklistItemUpdateInput): Promise<TesterChecklistItemDocument>;
|
|
10
|
+
listActive(): Promise<TesterChecklistItemDocument[]>;
|
|
11
|
+
list(model: SieveModel): Promise<FirebaseSieveResult<TesterChecklistItemDocument>>;
|
|
12
|
+
}
|
|
13
|
+
export declare const testerChecklistItemRepository: TesterChecklistItemRepository;
|