@mohasinac/appkit 4.61.0 → 4.62.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 +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
- package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
- package/dist/_internal/shared/actions/action-registry.js +149 -9
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/constants/api-endpoints.d.ts +3 -0
- package/dist/constants/api-endpoints.js +1 -0
- package/dist/constants/field-names.d.ts +17 -0
- package/dist/constants/field-names.js +23 -0
- package/dist/core/server-action.js +19 -3
- package/dist/errors/error-mapping.js +26 -1
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/admin/components/AdminActionIndexView.js +100 -5
- package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselView.js +96 -4
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminMediaView.js +36 -3
- package/dist/features/admin/components/AdminNavigationView.js +4 -1
- package/dist/features/admin/components/AdminOffersView.js +2 -1
- package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
- package/dist/features/admin/components/AdminProductEditorView.js +2 -2
- package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
- package/dist/features/admin/components/AdminSectionsView.js +486 -224
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
- package/dist/features/admin/repository/notification.repository.d.ts +15 -0
- package/dist/features/admin/repository/notification.repository.js +32 -0
- package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -0
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/events/components/AdminEventEditorView.js +8 -6
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/RefundRequestView.js +4 -4
- package/dist/features/orders/schemas/index.d.ts +6 -6
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- package/dist/features/seller/components/FulfillmentView.js +3 -2
- package/dist/features/seller/components/SellerAddressesView.js +32 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
- package/dist/features/seller/components/SellerBundlesView.js +53 -14
- package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
- package/dist/features/seller/components/SellerListingTypeView.js +56 -10
- package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
- package/dist/features/seller/components/SellerProductsView.js +5 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
- package/dist/features/seller/components/SellerShippingView.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
- package/dist/features/seller/components/SellerSupportView.js +69 -2
- package/dist/features/shell/SectionForm.js +20 -16
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
- package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/next/routing/route-map.d.ts +4 -0
- package/dist/next/routing/route-map.js +9 -0
- package/dist/schemas/registry.d.ts +4 -4
- package/dist/schemas/sieve.d.ts +3 -3
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/ui/components/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +43 -9
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/ui/forms/FormErrorList.d.ts +0 -16
- package/dist/ui/forms/FormErrorList.js +34 -1
- package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
- package/dist/ui/forms/apply-zod-issues.js +23 -1
- package/package.json +1 -1
|
@@ -690,6 +690,7 @@ export const SELLER_ENDPOINTS = {
|
|
|
690
690
|
BUNDLES: "/api/store/bundles",
|
|
691
691
|
/** Tickets raised ABOUT this store (W11). */
|
|
692
692
|
SUPPORT: "/api/store/support",
|
|
693
|
+
SUPPORT_TICKET_BY_ID: (id) => `/api/store/support/${id}`,
|
|
693
694
|
BUNDLE_BY_ID: (id) => `/api/store/bundles/${id}`,
|
|
694
695
|
ORDERS_ASSIGN: (orderId) => `/api/store/orders/${orderId}/assign`,
|
|
695
696
|
PRODUCT_BY_ID: (id) => `/api/store/products/${id}`,
|
|
@@ -982,6 +982,23 @@ export declare const COMMON_FIELDS: {
|
|
|
982
982
|
readonly IS_ACTIVE: "isActive";
|
|
983
983
|
readonly ORDER: "order";
|
|
984
984
|
};
|
|
985
|
+
/**
|
|
986
|
+
* The idempotency rows behind a refund.
|
|
987
|
+
*
|
|
988
|
+
* 🛑 `STATE` is the claim's own lifecycle ("claimed" → settled), NOT a
|
|
989
|
+
* postal state. `audit-route-nav-field-constants` matches a raw field
|
|
990
|
+
* string against every constant that happens to share its VALUE, so it
|
|
991
|
+
* suggested `ADDRESS_FIELDS.STATE` here — right string, wrong concept, and
|
|
992
|
+
* taking the suggestion would have made the query read as address filtering.
|
|
993
|
+
*/
|
|
994
|
+
export declare const REFUND_CLAIM_FIELDS: {
|
|
995
|
+
readonly STATE: "state";
|
|
996
|
+
readonly CLAIMED_AT: "claimedAt";
|
|
997
|
+
};
|
|
998
|
+
/** Observed market prices, keyed by the taxonomy node they were seen under. */
|
|
999
|
+
export declare const PRICE_OBSERVATION_FIELDS: {
|
|
1000
|
+
readonly CATEGORY_ID: "categoryId";
|
|
1001
|
+
};
|
|
985
1002
|
export declare const OAUTH_STATE_VALUES: {
|
|
986
1003
|
readonly PENDING: "pending";
|
|
987
1004
|
readonly USED: "used";
|
|
@@ -1175,6 +1175,29 @@ export const COMMON_FIELDS = {
|
|
|
1175
1175
|
ORDER: "order",
|
|
1176
1176
|
};
|
|
1177
1177
|
// ============================================================================
|
|
1178
|
+
// REFUND CLAIMS
|
|
1179
|
+
// ============================================================================
|
|
1180
|
+
/**
|
|
1181
|
+
* The idempotency rows behind a refund.
|
|
1182
|
+
*
|
|
1183
|
+
* 🛑 `STATE` is the claim's own lifecycle ("claimed" → settled), NOT a
|
|
1184
|
+
* postal state. `audit-route-nav-field-constants` matches a raw field
|
|
1185
|
+
* string against every constant that happens to share its VALUE, so it
|
|
1186
|
+
* suggested `ADDRESS_FIELDS.STATE` here — right string, wrong concept, and
|
|
1187
|
+
* taking the suggestion would have made the query read as address filtering.
|
|
1188
|
+
*/
|
|
1189
|
+
export const REFUND_CLAIM_FIELDS = {
|
|
1190
|
+
STATE: "state",
|
|
1191
|
+
CLAIMED_AT: "claimedAt",
|
|
1192
|
+
};
|
|
1193
|
+
// ============================================================================
|
|
1194
|
+
// PRICE OBSERVATIONS
|
|
1195
|
+
// ============================================================================
|
|
1196
|
+
/** Observed market prices, keyed by the taxonomy node they were seen under. */
|
|
1197
|
+
export const PRICE_OBSERVATION_FIELDS = {
|
|
1198
|
+
CATEGORY_ID: "categoryId",
|
|
1199
|
+
};
|
|
1200
|
+
// ============================================================================
|
|
1178
1201
|
// OAUTH STATE VALUES
|
|
1179
1202
|
// ============================================================================
|
|
1180
1203
|
export const OAUTH_STATE_VALUES = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeError } from "../errors/normalize.js";
|
|
2
2
|
import { toApiIssues } from "../errors/error-envelope.js";
|
|
3
|
-
import { HTTP_ERROR_CODES } from "../errors/error-mapping.js";
|
|
3
|
+
import { HTTP_ERROR_CODES, mapToHttpError } from "../errors/error-mapping.js";
|
|
4
4
|
/**
|
|
5
5
|
* createServerAction — @mohasinac/appkit/core
|
|
6
6
|
*
|
|
@@ -97,11 +97,27 @@ export function createServerAction(options) {
|
|
|
97
97
|
}
|
|
98
98
|
catch (err) {
|
|
99
99
|
void normalizeError(err);
|
|
100
|
-
|
|
100
|
+
/*
|
|
101
|
+
* 🛑 This never got the Rule #9 fix its sibling `wrapAction` did.
|
|
102
|
+
*
|
|
103
|
+
* It took `err.message` verbatim — unscrubbed for 5xx, so a thrown
|
|
104
|
+
* Node/bundler error's internals reached the client — and emitted NO
|
|
105
|
+
* `code`, so `toUserMessage(result.code)` downstream could only ever
|
|
106
|
+
* resolve to the generic sentence.
|
|
107
|
+
*
|
|
108
|
+
* It has no in-repo callers, but it is exported from the public barrel,
|
|
109
|
+
* so it is a trap for the next consumer rather than a live bug. Route it
|
|
110
|
+
* through the same mapper `wrapAction` uses: `mapToHttpError` scrubs 5xx
|
|
111
|
+
* messages in production and carries the real one in `internalMessage`
|
|
112
|
+
* for recorders only.
|
|
113
|
+
*/
|
|
114
|
+
const mapped = mapToHttpError(err, {
|
|
115
|
+
isProduction: process.env.NODE_ENV === "production",
|
|
116
|
+
});
|
|
101
117
|
logger.error(`[createServerAction] handler error in "${actionName}"`, {
|
|
102
118
|
err,
|
|
103
119
|
});
|
|
104
|
-
result = { ok: false, error: message };
|
|
120
|
+
result = { ok: false, code: mapped.code, error: mapped.message };
|
|
105
121
|
}
|
|
106
122
|
// -- 4. After hooks (per-action → global) ------------------------------
|
|
107
123
|
try {
|
|
@@ -42,6 +42,31 @@ function isZodLikeError(err) {
|
|
|
42
42
|
"issues" in err &&
|
|
43
43
|
Array.isArray(err.issues));
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The issues carried by a `ValidationError`, whichever of the two shapes the
|
|
47
|
+
* thrower used.
|
|
48
|
+
*
|
|
49
|
+
* 🛑 `new ValidationError(msg, parsed.error.issues)` silently lost everything.
|
|
50
|
+
* `ValidationError` forwards its second argument to `AppError.data`, and the
|
|
51
|
+
* old test was `isZodLikeError(err.data)` — which requires `"issues" in data`.
|
|
52
|
+
* An ARRAY has no `.issues` property (`"issues" in [] === false`), so passing
|
|
53
|
+
* `parsed.error.issues` (the obvious thing to pass, and what four routes do)
|
|
54
|
+
* produced a 400 with no field information at all. Only passing the whole
|
|
55
|
+
* `ZodError` worked.
|
|
56
|
+
*
|
|
57
|
+
* `media/finalize/route.ts` even carried a comment asserting the opposite —
|
|
58
|
+
* "carries `issues` through to the client, so the caller learns WHICH field it
|
|
59
|
+
* got wrong". It did not.
|
|
60
|
+
*
|
|
61
|
+
* Accept both, so the next caller cannot get it wrong either way.
|
|
62
|
+
*/
|
|
63
|
+
function issuesFromErrorData(data) {
|
|
64
|
+
if (Array.isArray(data))
|
|
65
|
+
return data.length > 0 ? data : undefined;
|
|
66
|
+
if (isZodLikeError(data))
|
|
67
|
+
return data.issues;
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
45
70
|
function isFirestoreLikeError(err) {
|
|
46
71
|
if (typeof err !== "object" || err === null)
|
|
47
72
|
return false;
|
|
@@ -110,7 +135,7 @@ function classifyError(err, opts) {
|
|
|
110
135
|
status: 400,
|
|
111
136
|
code: HTTP_ERROR_CODES.VALIDATION_FAILED,
|
|
112
137
|
message: err.message,
|
|
113
|
-
issues:
|
|
138
|
+
issues: issuesFromErrorData(err.data),
|
|
114
139
|
};
|
|
115
140
|
}
|
|
116
141
|
if (err instanceof AuthenticationError) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useState, useTransition } from "react";
|
|
4
|
-
import { Alert, Badge, Button, Div, Heading, LoginRequiredModal, Row, Spinner, Stack, Text } from "../../../ui/index.js";
|
|
4
|
+
import { Alert, Badge, Button, Div, Heading, LoginRequiredModal, Row, Spinner, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
5
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
5
6
|
import { isAuthError } from "../../../utils/auth-error.js";
|
|
6
7
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
7
8
|
import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
8
9
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
10
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
9
11
|
const CLS_COUNTER_AMOUNT = "text-[length:var(--appkit-text-sm)] font-semibold text-info dark:text-info";
|
|
10
12
|
function formatRupees(amount) {
|
|
11
13
|
if (amount === undefined || amount === null)
|
|
@@ -60,7 +62,7 @@ function BuyerOfferCard({ offer, onAcceptCounter, onWithdraw, onCheckout, onUpda
|
|
|
60
62
|
}
|
|
61
63
|
});
|
|
62
64
|
}
|
|
63
|
-
return (_jsxs(Stack, { surface: "card", padding: "sm", gap: "3", children: [_jsxs(Row, { align: "start", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(
|
|
65
|
+
return (_jsxs(Stack, { surface: "card", padding: "sm", gap: "3", children: [_jsxs(Row, { align: "start", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(TextLink, { href: String(ROUTES.PUBLIC.PRODUCT_DETAIL(offer.productSlug ?? offer.productId)), size: "sm", weight: "semibold", truncate: true, children: offer.productTitle ?? "Unknown Product" }), _jsx(Text, { className: "mt-0.5", color: "muted", size: "xs", children: relativeTime(offer.createdAt) })] }), _jsx(Badge, { variant: statusVariant(offer.status), size: "xs", className: "shrink-0 capitalize", children: offer.status })] }), _jsxs(Row, { wrap: true, gap: "md", children: [_jsxs(Div, { children: [_jsx(Text, { className: "tracking-wide", color: "faint", size: "xs", transform: "uppercase", children: "Listed" }), _jsx(Text, { size: "sm", weight: "medium", color: "muted", children: formatRupees(offer.listedPrice) })] }), _jsxs(Div, { children: [_jsx(Text, { className: "tracking-wide", color: "faint", size: "xs", transform: "uppercase", children: "Your Offer" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatRupees(offer.offerAmount) })] }), offer.counterAmount != null && (_jsxs(Div, { children: [_jsx(Text, { className: "tracking-wide", color: "faint", size: "xs", transform: "uppercase", children: "Seller Counter" }), _jsx(Text, { className: CLS_COUNTER_AMOUNT, children: formatRupees(offer.counterAmount) })] })), offer.lockedPrice != null && (_jsxs(Div, { children: [_jsx(Text, { className: "tracking-wide", color: "faint", size: "xs", transform: "uppercase", children: "Agreed Price" }), _jsx(Text, { className: "text-success", size: "sm", weight: "semibold", children: formatRupees(offer.lockedPrice) })] }))] }), offer.sellerNote && (_jsxs(Text, { className: "italic", color: "muted", size: "xs", children: ["Seller note: \"", offer.sellerNote, "\""] })), offer.status === "countered" && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { className: "text-info", size: "xs", weight: "medium", children: ["Seller countered at ", formatRupees(offer.counterAmount), ". Accept or withdraw your offer."] }), _jsxs(Row, { wrap: true, gap: "sm", children: [_jsx(Button, { size: "sm", variant: "primary", onClick: () => act(() => onAcceptCounter(offer.id), { status: "accepted", lockedPrice: offer.counterAmount }), disabled: isPending, children: isPending ? "Accepting…" : `Accept ${formatRupees(offer.counterAmount)}` }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setConfirming("withdraw"), disabled: isPending, className: "text-error border border-error/20", children: "Withdraw" })] })] })), offer.status === "accepted" && onCheckout && (_jsx(Button, { size: "sm", variant: "primary", onClick: () => act(() => onCheckout(offer.id), { status: "paid" }), disabled: isPending, children: isPending ? "Processing…" : "Checkout at Agreed Price" })), offer.status === "pending" && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setConfirming("withdraw"), disabled: isPending, className: "text-error border border-error/20 text-[length:var(--appkit-text-xs)]", children: "Withdraw Offer" })), confirming === "withdraw" && (_jsxs(Row, { gap: "sm", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => act(() => onWithdraw(offer.id), { status: "withdrawn" }), disabled: isPending, className: "text-error border border-error/20", children: isPending ? "Withdrawing…" : "Confirm Withdraw" }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setConfirming(null), disabled: isPending, children: "Cancel" })] })), error && _jsx(Alert, { variant: "error", children: _jsx(Text, { size: "xs", children: error }) })] }));
|
|
64
66
|
}
|
|
65
67
|
export function UserOffersPanel({ fetchEndpoint = ACCOUNT_ENDPOINTS.OFFERS, onAcceptCounter, onWithdraw, onCheckout, className = "", }) {
|
|
66
68
|
const [offers, setOffers] = useState([]);
|
|
@@ -92,7 +94,7 @@ export function UserOffersPanel({ fetchEndpoint = ACCOUNT_ENDPOINTS.OFFERS, onAc
|
|
|
92
94
|
}
|
|
93
95
|
catch (err) {
|
|
94
96
|
void normalizeError(err);
|
|
95
|
-
setFetchError(err
|
|
97
|
+
setFetchError(toUserMessage(err?.code, undefined, { fallback: "Failed to load offers." }));
|
|
96
98
|
}
|
|
97
99
|
finally {
|
|
98
100
|
setLoading(false);
|
|
@@ -27,10 +27,12 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
27
27
|
import React from "react";
|
|
28
28
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
29
29
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
30
|
-
import { Alert, Badge, Row, Span, Stack, Text, Toggle, useToast } from "../../../ui/index.js";
|
|
30
|
+
import { Alert, Badge, Row, Span, Stack, Text, TextLink, Toggle, useToast } from "../../../ui/index.js";
|
|
31
31
|
import { apiClient } from "../../../http/index.js";
|
|
32
32
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
33
33
|
import { DataListingView } from "./DataListingView.js";
|
|
34
|
+
import { QuickEditMenu } from "./QuickEditMenu.js";
|
|
35
|
+
import { actionIndexOverrideSchema, splitKeywords, joinKeywords, ACTION_INDEX_OVERRIDE_FIELDS, } from "../schemas/action-index-form.js";
|
|
34
36
|
const ENDPOINT = ADMIN_ENDPOINTS.ACTION_INDEX;
|
|
35
37
|
export function AdminActionIndexView({ labels = {} }) {
|
|
36
38
|
const queryClient = useQueryClient();
|
|
@@ -41,7 +43,12 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
41
43
|
staleTime: 60000,
|
|
42
44
|
});
|
|
43
45
|
const payload = query.data?.data;
|
|
44
|
-
|
|
46
|
+
/*
|
|
47
|
+
* Memoised because the `?? {}` fallback is a fresh object every render,
|
|
48
|
+
* which makes the `rows` memo below rebuild on every render — it reads
|
|
49
|
+
* `overrides` for each entry's effective `enabled`.
|
|
50
|
+
*/
|
|
51
|
+
const overrides = React.useMemo(() => payload?.control?.entries ?? {}, [payload?.control?.entries]);
|
|
45
52
|
const toggleMutation = useApiMutation({
|
|
46
53
|
errorMessage: "Could not update that entry.",
|
|
47
54
|
mutationFn: async (vars) => apiClient.patch(ENDPOINT, {
|
|
@@ -53,6 +60,44 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
53
60
|
void queryClient.invalidateQueries({ queryKey: ["admin", "action-index"] });
|
|
54
61
|
},
|
|
55
62
|
});
|
|
63
|
+
/*
|
|
64
|
+
* Spread the EXISTING override first, exactly as the toggle does. The
|
|
65
|
+
* endpoint's `setOverride` replaces the whole override object, so sending
|
|
66
|
+
* only the edited fields would silently clear this entry's `enabled` —
|
|
67
|
+
* re-showing something an admin had deliberately hidden.
|
|
68
|
+
*/
|
|
69
|
+
const overrideMutation = useApiMutation({
|
|
70
|
+
errorMessage: "Could not save that entry.",
|
|
71
|
+
mutationFn: async (vars) => {
|
|
72
|
+
const parsed = actionIndexOverrideSchema.parse(vars.values);
|
|
73
|
+
return apiClient.patch(ENDPOINT, {
|
|
74
|
+
entryId: vars.entryId,
|
|
75
|
+
override: {
|
|
76
|
+
...overrides[vars.entryId],
|
|
77
|
+
label: parsed.label,
|
|
78
|
+
description: parsed.description,
|
|
79
|
+
keywords: splitKeywords(parsed.keywords),
|
|
80
|
+
weight: parsed.weight,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
onSuccess: () => {
|
|
85
|
+
showToast("Entry saved.", "success");
|
|
86
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "action-index"] });
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
/** Named so the row-action array stays a flat list of declarations. */
|
|
90
|
+
const saveOverride = React.useCallback(async (entryId, values) => {
|
|
91
|
+
await overrideMutation.mutateAsync({ entryId, values });
|
|
92
|
+
}, [overrideMutation]);
|
|
93
|
+
const clearMutation = useApiMutation({
|
|
94
|
+
errorMessage: "Could not reset that entry.",
|
|
95
|
+
mutationFn: async (entryId) => apiClient.patch(ENDPOINT, { entryId, clear: true }),
|
|
96
|
+
onSuccess: () => {
|
|
97
|
+
showToast("Entry reset to its code default.", "success");
|
|
98
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "action-index"] });
|
|
99
|
+
},
|
|
100
|
+
});
|
|
56
101
|
/*
|
|
57
102
|
* Ordered by weight, then label — the same ranking `matchesNavQuery` gives
|
|
58
103
|
* the search, so the admin reading this list sees entries in the order a
|
|
@@ -69,6 +114,10 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
69
114
|
href: `${entry.href}${entry.deepLink ?? ""}`,
|
|
70
115
|
permission: entry.requiredPermission ?? "—",
|
|
71
116
|
enabled: overrides[entry.id]?.enabled !== false,
|
|
117
|
+
// Seeds the edit drawer. `entry.*` is already override-merged by
|
|
118
|
+
// `mergeActionIndex`, so these are the effective values an admin sees.
|
|
119
|
+
keywords: joinKeywords(entry.keywords),
|
|
120
|
+
weight: entry.weight ?? 0,
|
|
72
121
|
})), [payload?.entries, overrides]);
|
|
73
122
|
const sync = payload?.sync;
|
|
74
123
|
return (_jsxs(Stack, { gap: "md", children: [sync && sync.orphanedOverrides.length > 0 && (_jsxs(Alert, { variant: "warning", title: "Overrides with no entry", children: [sync.orphanedOverrides.length, " saved override", sync.orphanedOverrides.length === 1 ? "" : "s", " point at entries the code no longer has (", sync.orphanedOverrides.slice(0, 4).join(", "), sync.orphanedOverrides.length > 4 ? "…" : "", "). They are ignored \u2014 clear them when convenient."] })), _jsx(DataListingView, { config: {
|
|
@@ -109,9 +158,19 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
109
158
|
},
|
|
110
159
|
{ key: "portal", header: "Portal" },
|
|
111
160
|
{
|
|
161
|
+
/*
|
|
162
|
+
* A LINK, not mono text.
|
|
163
|
+
*
|
|
164
|
+
* This row IS a destination — "where is it" is half of what
|
|
165
|
+
* this screen exists to answer, and the answer was printed as
|
|
166
|
+
* inert text beside a toggle. The link is also what makes the
|
|
167
|
+
* row reachable at all: every other column is metadata about
|
|
168
|
+
* the entry, so without it there is no way to go look at the
|
|
169
|
+
* thing being enabled or disabled.
|
|
170
|
+
*/
|
|
112
171
|
key: "href",
|
|
113
172
|
header: "Goes to",
|
|
114
|
-
render: (row) => (_jsx(
|
|
173
|
+
render: (row) => (_jsx(TextLink, { href: row.href, size: "xs", onClick: (e) => e.stopPropagation(), children: _jsx(Span, { size: "xs", family: "mono", children: row.href }) })),
|
|
115
174
|
},
|
|
116
175
|
{
|
|
117
176
|
key: "permission",
|
|
@@ -121,8 +180,44 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
121
180
|
{
|
|
122
181
|
key: "enabled",
|
|
123
182
|
header: "Shown",
|
|
124
|
-
render: (row) => (_jsx(Row, { align: "center", gap: "sm", children: _jsx(Toggle, { checked: row.enabled, size: "sm",
|
|
183
|
+
render: (row) => (_jsx(Row, { align: "center", gap: "sm", children: _jsx(Toggle, { checked: row.enabled, size: "sm", ariaLabel: `Show ${row.primary} in navigation and search`, onChange: (next) => toggleMutation.mutate({ entryId: row.id, enabled: next }) }) })),
|
|
125
184
|
},
|
|
126
185
|
],
|
|
127
|
-
|
|
186
|
+
/*
|
|
187
|
+
* 🛑 This exists because the footer below already PROMISED it:
|
|
188
|
+
* "Labels, descriptions and keywords can be edited here." They
|
|
189
|
+
* could not. `PATCH /api/admin/action-index` has always accepted
|
|
190
|
+
* `label` / `description` / `keywords` / `weight` in an override
|
|
191
|
+
* (see `ActionIndexOverride`), and the only control on this screen
|
|
192
|
+
* was the Shown toggle — so the sentence described a capability the
|
|
193
|
+
* server had and the UI had never grown. Root Cause #51's shape: a
|
|
194
|
+
* label describing something other than what the surface does.
|
|
195
|
+
*
|
|
196
|
+
* `href`, `kind` and every permission field stay out, per the
|
|
197
|
+
* component docstring: repointing a built-in route is routing, and
|
|
198
|
+
* relaxing a permission is access control.
|
|
199
|
+
*/
|
|
200
|
+
renderRowActions: (row) => (_jsx(QuickEditMenu, { actions: [
|
|
201
|
+
{
|
|
202
|
+
label: "Edit entry",
|
|
203
|
+
formTitle: `Edit "${row.primary}"`,
|
|
204
|
+
schema: actionIndexOverrideSchema,
|
|
205
|
+
fields: ACTION_INDEX_OVERRIDE_FIELDS,
|
|
206
|
+
defaultValues: {
|
|
207
|
+
label: row.primary,
|
|
208
|
+
description: row.secondary,
|
|
209
|
+
keywords: row.keywords,
|
|
210
|
+
weight: row.weight,
|
|
211
|
+
},
|
|
212
|
+
onSubmit: (values) => saveOverride(row.id, values),
|
|
213
|
+
submitLabel: "Save entry",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
label: "Reset to code default",
|
|
217
|
+
separator: true,
|
|
218
|
+
destructive: true,
|
|
219
|
+
onClick: () => clearMutation.mutate(row.id),
|
|
220
|
+
},
|
|
221
|
+
] })),
|
|
222
|
+
} }), _jsx(Text, { size: "xs", color: "muted", children: "Hiding an entry removes it from the sidebar, the header search and the command palette at once. Use the row menu to edit an entry\u2019s label, description, keywords and search weight, or to reset it to its code default; a route and its required permission belong to the code." })] }));
|
|
128
223
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
-
import { Badge, Button, Div, Heading, Row, Span, Stack, Text, useToast } from "../../../ui/index.js";
|
|
5
|
+
import { Badge, Button, Div, Heading, Row, Span, Stack, Text, TextLink, useToast } from "../../../ui/index.js";
|
|
6
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
6
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
7
8
|
import { apiClient } from "../../../http/index.js";
|
|
8
9
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
@@ -31,6 +32,24 @@ export function AdminAddressClustersView(_props) {
|
|
|
31
32
|
void queryClient.invalidateQueries({ queryKey: ["admin", "address-clusters"] });
|
|
32
33
|
},
|
|
33
34
|
});
|
|
35
|
+
/*
|
|
36
|
+
* 🛑 "Flag Suspicious" was ONE-WAY: there was no unflag, so a wrong call —
|
|
37
|
+
* or a cluster that turned out to be a family sharing an address — left a
|
|
38
|
+
* permanent mark with no way back from this screen.
|
|
39
|
+
*
|
|
40
|
+
* `clear_ban` has existed on the route the whole time and calls
|
|
41
|
+
* `clearBanById`; only the control was missing.
|
|
42
|
+
*/
|
|
43
|
+
const clearFlagMutation = useApiMutation({
|
|
44
|
+
errorMessage: "Failed to clear that flag.",
|
|
45
|
+
mutationFn: async (id) => {
|
|
46
|
+
await apiClient.patch(ADMIN_ENDPOINTS.ADDRESS_BY_ID(id), { action: "clear_ban" });
|
|
47
|
+
},
|
|
48
|
+
onSuccess: () => {
|
|
49
|
+
showToast("Flag cleared.", "success");
|
|
50
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "address-clusters"] });
|
|
51
|
+
},
|
|
52
|
+
});
|
|
34
53
|
const clusters = data?.clusters ?? [];
|
|
35
|
-
return (_jsxs(Stack, { gap: "lg", padding: "page", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", color: "primary", children: "Address Clusters" }), _jsx(Text, { size: "sm", color: "muted", className: "mt-1", children: "Users sharing the same physical address. Flagging is informational only \u2014 users are not blocked." })] }), isLoading && (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), error && (_jsx(Div, { surface: "danger-surface", color: "error", padding: "inline", rounded: "xl", textSize: "sm", children: "Failed to load clusters." })), !isLoading && clusters.length === 0 && (_jsx(Div, { surface: "muted", padding: "y-4xl", rounded: "xl", className: "text-center", children: _jsx(Text, { color: "muted", size: "sm", children: "No shared addresses found." }) })), clusters.map((cluster) => (_jsxs(Stack, { surface: "card", padding: "md", gap: "md", rounded: "xl", border: "default", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: "Hash" }), _jsx(Span, { size: "xs", color: "muted", className: "font-mono truncate max-w-xs", children: cluster.addressHash })] }), _jsxs(Badge, { variant: "secondary", size: "sm", children: [cluster.addresses.length, " accounts"] })] }), _jsx(Stack, { gap: "xs", children: cluster.addresses.map((addr) => (_jsxs(Row, { justify: "between", align: "center", gap: "sm", surface: "muted", padding: "inlineSm", rounded: "lg", children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(
|
|
54
|
+
return (_jsxs(Stack, { gap: "lg", padding: "page", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", color: "primary", children: "Address Clusters" }), _jsx(Text, { size: "sm", color: "muted", className: "mt-1", children: "Users sharing the same physical address. Flagging is informational only \u2014 users are not blocked." })] }), isLoading && (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), error && (_jsx(Div, { surface: "danger-surface", color: "error", padding: "inline", rounded: "xl", textSize: "sm", children: "Failed to load clusters." })), !isLoading && clusters.length === 0 && (_jsx(Div, { surface: "muted", padding: "y-4xl", rounded: "xl", className: "text-center", children: _jsx(Text, { color: "muted", size: "sm", children: "No shared addresses found." }) })), clusters.map((cluster) => (_jsxs(Stack, { surface: "card", padding: "md", gap: "md", rounded: "xl", border: "default", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: "Hash" }), _jsx(Span, { size: "xs", color: "muted", className: "font-mono truncate max-w-xs", children: cluster.addressHash })] }), _jsxs(Badge, { variant: "secondary", size: "sm", children: [cluster.addresses.length, " accounts"] })] }), _jsx(Stack, { gap: "xs", children: cluster.addresses.map((addr) => (_jsxs(Row, { justify: "between", align: "center", gap: "sm", surface: "muted", padding: "inlineSm", rounded: "lg", children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(TextLink, { href: addr.ownerType === "user" ? String(ROUTES.ADMIN.USER_DETAIL(addr.ownerId)) : String(ROUTES.ADMIN.STORE_DETAIL(addr.ownerId)), size: "sm", weight: "medium", truncate: true, children: [addr.ownerType === "user" ? "User" : "Store", ": ", addr.ownerId] }), addr.city && (_jsx(Text, { size: "xs", color: "muted", children: [addr.city, addr.state, addr.postalCode].filter(Boolean).join(", ") }))] }), _jsxs(Row, { gap: "xs", align: "center", children: [addr.banStatus && (_jsx(Badge, { variant: BAN_BADGE[addr.banStatus] ?? "secondary", size: "sm", children: addr.banStatus.replace(/_/g, " ") })), addr.banStatus === "suspicious" ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => clearFlagMutation.mutate(addr.id), disabled: clearFlagMutation.isPending, children: "Clear flag" })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => flagMutation.mutate(addr.id), disabled: flagMutation.isPending, children: "Flag Suspicious" }))] })] }, addr.id))) })] }, cluster.addressHash)))] }));
|
|
36
55
|
}
|
|
@@ -201,13 +201,13 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
201
201
|
required: true,
|
|
202
202
|
quick: true,
|
|
203
203
|
fields: ["title", "slug", "excerpt", "content"],
|
|
204
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Content" }), _jsx(Input, { label: "Title", value: values.title, onChange: (e) => {
|
|
204
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Content" }), _jsx(Input, { label: "Title", error: errors.title, value: values.title, onChange: (e) => {
|
|
205
205
|
const v = e.target.value;
|
|
206
206
|
onChange({ title: v, ...(!slugManual && { slug: toSlug(v) }) });
|
|
207
|
-
}, placeholder: "e.g. How to Grade Pok\u00E9mon Cards" }), _jsx(Input, { label: "Slug", value: values.slug, onChange: (e) => {
|
|
207
|
+
}, placeholder: "e.g. How to Grade Pok\u00E9mon Cards" }), _jsx(Input, { label: "Slug", error: errors.slug, value: values.slug, onChange: (e) => {
|
|
208
208
|
setSlugManual(true);
|
|
209
209
|
onChange({ slug: e.target.value });
|
|
210
|
-
}, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsx(Input, { label: "Excerpt", value: values.excerpt, onChange: (e) => onChange({ excerpt: e.target.value }), placeholder: "Short summary shown in listings and cards" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Content" }), _jsx(RichTextEditor, { ariaLabel: "Content", value: values.content, onChange: (v) => onChange({ content: v }), placeholder: "Write your article here...", minHeightClassName: "min-h-[320px]" })] })] })),
|
|
210
|
+
}, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsx(Input, { label: "Excerpt", error: errors.excerpt, value: values.excerpt, onChange: (e) => onChange({ excerpt: e.target.value }), placeholder: "Short summary shown in listings and cards" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Content" }), _jsx(RichTextEditor, { ariaLabel: "Content", value: values.content, onChange: (v) => onChange({ content: v }), placeholder: "Write your article here...", minHeightClassName: "min-h-[320px]" })] })] })),
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
id: "media",
|
|
@@ -216,20 +216,20 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
216
216
|
// its children, which would abort an in-flight upload.
|
|
217
217
|
keepMounted: true,
|
|
218
218
|
fields: ["coverImage", "youtubeId"],
|
|
219
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Media" }), _jsx(ImageUpload, { label: "Cover Image", currentImage: values.coverImage, onUpload: (file) => upload(file, "blog", true, {
|
|
219
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Media" }), _jsx(ImageUpload, { label: "Cover Image", currentImage: values.coverImage, onUpload: (file) => upload(file, "blog", true, {
|
|
220
220
|
type: "blog-cover",
|
|
221
221
|
title: values.title || values.slug,
|
|
222
222
|
category: values.category,
|
|
223
|
-
}), onChange: (url) => onChange({ coverImage: url }) }), _jsx(Input, { label: "YouTube Video ID (optional)", value: values.youtubeId, onChange: (e) => onChange({ youtubeId: e.target.value }), placeholder: "e.g. dQw4w9WgXcQ", helperText: "The 11-character video ID from the YouTube URL." })] })),
|
|
223
|
+
}), onChange: (url) => onChange({ coverImage: url }) }), _jsx(Input, { label: "YouTube Video ID (optional)", error: errors.youtubeId, value: values.youtubeId, onChange: (e) => onChange({ youtubeId: e.target.value }), placeholder: "e.g. dQw4w9WgXcQ", helperText: "The 11-character video ID from the YouTube URL." })] })),
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
226
|
id: "seo",
|
|
227
227
|
label: "SEO & Tags",
|
|
228
228
|
quick: true,
|
|
229
229
|
fields: ["category", "tags", "metaTitle", "metaDescription"],
|
|
230
|
-
render: ({ values, onChange }) => {
|
|
230
|
+
render: ({ values, onChange, errors }) => {
|
|
231
231
|
const readTime = Math.max(1, Math.round(values.content.replace(/<[^>]*>/g, "").split(/\s+/).filter(Boolean).length / 200));
|
|
232
|
-
return (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "SEO & Tags" }), _jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-2", children: [_jsx(Select, { label: "Category", options: CATEGORY_OPTIONS, value: values.category, onValueChange: (v) => onChange({ category: v }) }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-1.5", size: "sm", weight: "medium", children: "Estimated Read Time" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: ["~", readTime, " min (auto-calculated from content)"] })] })] }), _jsx(TagInput, { label: "Tags", value: values.tags, onChange: (t) => onChange({ tags: t }), placeholder: "e.g. pokemon, grading, tcg" }), _jsx(Input, { label: "Meta Title (optional)", value: values.metaTitle, onChange: (e) => onChange({ metaTitle: e.target.value }), placeholder: "Defaults to post title" }), _jsx(Input, { label: "Meta Description (optional)", value: values.metaDescription, onChange: (e) => onChange({ metaDescription: e.target.value }), placeholder: "SEO description \u2014 max 160 chars", maxLength: 160 })] }));
|
|
232
|
+
return (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "SEO & Tags" }), _jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-2", children: [_jsx(Select, { label: "Category", error: errors.category, options: CATEGORY_OPTIONS, value: values.category, onValueChange: (v) => onChange({ category: v }) }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-1.5", size: "sm", weight: "medium", children: "Estimated Read Time" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: ["~", readTime, " min (auto-calculated from content)"] })] })] }), _jsx(TagInput, { label: "Tags", error: errors.tags, value: values.tags, onChange: (t) => onChange({ tags: t }), placeholder: "e.g. pokemon, grading, tcg" }), _jsx(Input, { label: "Meta Title (optional)", error: errors.metaTitle, value: values.metaTitle, onChange: (e) => onChange({ metaTitle: e.target.value }), placeholder: "Defaults to post title" }), _jsx(Input, { label: "Meta Description (optional)", error: errors.metaDescription, value: values.metaDescription, onChange: (e) => onChange({ metaDescription: e.target.value }), placeholder: "SEO description \u2014 max 160 chars", maxLength: 160 })] }));
|
|
233
233
|
},
|
|
234
234
|
},
|
|
235
235
|
{
|
|
@@ -237,7 +237,7 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
237
237
|
label: "Publish",
|
|
238
238
|
quick: true,
|
|
239
239
|
fields: ["status", "publishedAt", "authorName", "isFeatured"],
|
|
240
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Publish Settings" }), _jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-2", children: [_jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: values.status, onValueChange: (v) => onChange({ status: v }) }), _jsx(Input, { label: "Publish date (optional)", value: values.publishedAt, onChange: (e) => onChange({ publishedAt: e.target.value }), type: "date", helperText: "Auto-set to now when publishing." })] }), _jsx(Input, { label: "Author Name", value: values.authorName, onChange: (e) => onChange({ authorName: e.target.value }), placeholder: "Author display name" }), _jsx(Toggle, { label: "Featured post", checked: values.isFeatured, onChange: (checked) => onChange({ isFeatured: checked }) }), isEdit && (_jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: _jsx(Button, { type: "button", variant: "danger", size: "sm", onClick: () => setShowDeleteConfirm(true), isLoading: deleteMutation.isPending, children: "Delete post" }) }))] })),
|
|
240
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Publish Settings" }), _jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-2", children: [_jsx(Select, { label: "Status", error: errors.status, options: STATUS_OPTIONS, value: values.status, onValueChange: (v) => onChange({ status: v }) }), _jsx(Input, { label: "Publish date (optional)", error: errors.publishedAt, value: values.publishedAt, onChange: (e) => onChange({ publishedAt: e.target.value }), type: "date", helperText: "Auto-set to now when publishing." })] }), _jsx(Input, { label: "Author Name", error: errors.authorName, value: values.authorName, onChange: (e) => onChange({ authorName: e.target.value }), placeholder: "Author display name" }), _jsx(Toggle, { label: "Featured post", checked: values.isFeatured, onChange: (checked) => onChange({ isFeatured: checked }) }), isEdit && (_jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: _jsx(Button, { type: "button", variant: "danger", size: "sm", onClick: () => setShowDeleteConfirm(true), isLoading: deleteMutation.isPending, children: "Delete post" }) }))] })),
|
|
241
241
|
},
|
|
242
242
|
];
|
|
243
243
|
const deleteModal = (_jsx(ConfirmDeleteModal, { isOpen: showDeleteConfirm, onConfirm: () => {
|
|
@@ -224,7 +224,7 @@ export function AdminCarouselEditorView({ slideId, onSaved, onDeleted, onCancel,
|
|
|
224
224
|
label: "Slide info",
|
|
225
225
|
required: true,
|
|
226
226
|
fields: ["title", "active", "order", "settings"],
|
|
227
|
-
render: () => (_jsxs(Stack, { gap: "md", children: [_jsx(FieldInput, { name: "title", label: "Title", value: title, onChange: (v) => setTitle(v), required: true, placeholder: "e.g. Hot Wheels RLC Exclusives" }), _jsx(Toggle, { label: "Active (visible on homepage)", checked: active, onChange: setActive }), _jsx(Input, { label: "Display order", type: "number", value: order, onChange: (e) => setOrder(e.target.value), min: 0, placeholder: "1" }), _jsx(Select, { label: "Slide height", value: height, onChange: (e) => setHeight(e.target.value), options: HEIGHT_OPTIONS }), _jsx(Input, { label: "Autoplay delay (ms)", type: "number", value: autoplayDelayMs, onChange: (e) => setAutoplayDelayMs(e.target.value), min: 1000, max: 15000, placeholder: "4000", helperText: "How long this slide shows before auto-advancing. Default: 4000 ms." })] })),
|
|
227
|
+
render: () => (_jsxs(Stack, { gap: "md", children: [_jsx(FieldInput, { name: "title", label: "Title", value: title, onChange: (v) => setTitle(v), required: true, placeholder: "e.g. Hot Wheels RLC Exclusives" }), _jsx(Toggle, { label: "Active (visible on homepage)", checked: active, onChange: setActive }), _jsx(Input, { label: "Display order", error: form.shellCtx.errors.order, type: "number", value: order, onChange: (e) => setOrder(e.target.value), min: 0, placeholder: "1" }), _jsx(Select, { label: "Slide height", value: height, onChange: (e) => setHeight(e.target.value), options: HEIGHT_OPTIONS }), _jsx(Input, { label: "Autoplay delay (ms)", type: "number", value: autoplayDelayMs, onChange: (e) => setAutoplayDelayMs(e.target.value), min: 1000, max: 15000, placeholder: "4000", helperText: "How long this slide shows before auto-advancing. Default: 4000 ms." })] })),
|
|
228
228
|
},
|
|
229
229
|
{
|
|
230
230
|
id: "background",
|