@mohasinac/appkit 4.41.2 → 4.41.4

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.
@@ -17,6 +17,16 @@ import { apiClient } from "../../../http";
17
17
  const PAGE_SIZE = 50;
18
18
  const FILTER_KEYS = ["active"];
19
19
  const DEFAULT_SORT = "order";
20
+ function extractCarouselRows(response) {
21
+ if (Array.isArray(response))
22
+ return response;
23
+ if (!response || typeof response !== "object")
24
+ return undefined;
25
+ const obj = response;
26
+ if (Array.isArray(obj.data))
27
+ return obj.data;
28
+ return Array.isArray(obj.items) ? obj.items : undefined;
29
+ }
20
30
  export function AdminCarouselView({ children, onBulkDelete, ...props }) {
21
31
  const hasChildren = React.Children.count(children) > 0;
22
32
  const [draggingId, setDraggingId] = useState(null);
@@ -30,7 +40,7 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
30
40
  queryKey: ["admin", "carousel", "listing"],
31
41
  endpoint: `${HOMEPAGE_ENDPOINTS.CAROUSEL}?includeInactive=true`,
32
42
  mapRows: (response) => {
33
- const sourceItems = Array.isArray(response.data) ? response.data : response.items;
43
+ const sourceItems = extractCarouselRows(response);
34
44
  return toRecordArray(sourceItems)
35
45
  .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
36
46
  .map((item, index) => {
@@ -50,7 +60,13 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
50
60
  };
51
61
  });
52
62
  },
53
- getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
63
+ getTotal: (response, mappedRows) => {
64
+ if (Array.isArray(response) || !response || typeof response !== "object") {
65
+ return mappedRows.length;
66
+ }
67
+ const total = response.total;
68
+ return typeof total === "number" ? total : mappedRows.length;
69
+ },
54
70
  buildFilters: (filterState) => {
55
71
  const activeRaw = filterState.active;
56
72
  if (activeRaw === "true")
@@ -9,6 +9,24 @@ import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdmin
9
9
  import { DataListingView } from "./DataListingView";
10
10
  import { AdminCategoryEditorView } from "./AdminCategoryEditorView";
11
11
  import { ADMIN_BULK_ACTIONS, ROW_ACTION_META } from "../../products/constants/action-defs";
12
+ /** Rows, whichever of the three shapes the endpoint returns. */
13
+ function extractCategoryRows(response) {
14
+ if (Array.isArray(response))
15
+ return response;
16
+ if (!response || typeof response !== "object")
17
+ return undefined;
18
+ const obj = response;
19
+ if (Array.isArray(obj.data))
20
+ return obj.data;
21
+ return Array.isArray(obj.items) ? obj.items : undefined;
22
+ }
23
+ /** `total` only exists on the object shapes; a bare array has none. */
24
+ function extractCategoryTotal(response) {
25
+ if (Array.isArray(response) || !response || typeof response !== "object")
26
+ return undefined;
27
+ const total = response.total;
28
+ return typeof total === "number" ? total : undefined;
29
+ }
12
30
  const COLUMNS = [
13
31
  {
14
32
  key: "primary",
@@ -46,7 +64,7 @@ const ADMIN_CATEGORIES_CONFIG = {
46
64
  ],
47
65
  columns: COLUMNS,
48
66
  mapRows: (response) => {
49
- const sourceItems = Array.isArray(response.data) ? response.data : response.items;
67
+ const sourceItems = extractCategoryRows(response);
50
68
  return toRecordArray(sourceItems).map((item, index) => ({
51
69
  id: toStringValue(item.id, `category-${index}`),
52
70
  primary: toStringValue(item.name, "Untitled category"),
@@ -59,7 +77,7 @@ const ADMIN_CATEGORIES_CONFIG = {
59
77
  updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
60
78
  }));
61
79
  },
62
- getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
80
+ getTotal: (response, mappedRows) => extractCategoryTotal(response) ?? mappedRows.length,
63
81
  buildFilters: (state) => {
64
82
  const parts = [];
65
83
  if (state.isActive)
@@ -38,7 +38,7 @@ function FeaturedBundleCard({ bundle, showSavingsBadge = true, onBuyNow, }) {
38
38
  const cover = bundle.display?.coverImage;
39
39
  const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
40
40
  const discount = computeBundleDiscount(bundle.bundlePrice, bundle.bundleOriginalTotal);
41
- return (_jsxs(Stack, { gap: "none", surface: "default", rounded: "xl", border: "default", children: [_jsxs(Link, { href: href, className: "group block p-[var(--appkit-space-3)] hover:no-underline", children: [_jsxs(Div, { className: `relative mb-2 aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: [cover ? (_jsx(MediaImage, { src: cover, alt: bundle.name, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", className: "h-full w-full", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), showSavingsBadge && discount && (_jsx(Badge, { variant: "success", className: "absolute left-2 top-2", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }))] }), _jsx(Text, { className: "line-clamp-2", size: "sm", weight: "semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", wrap: true, children: [_jsx(Text, { size: "sm", weight: "bold", children: _jsx(GatedPrice, { label: "Sign in to see the bundle price", children: bundle.bundlePrice
41
+ return (_jsxs(Stack, { gap: "none", surface: "default", rounded: "xl", border: "default", children: [_jsxs(Link, { href: href, className: "group block p-[var(--appkit-space-3)] hover:no-underline", children: [_jsxs(Div, { className: `relative mb-2 aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: [cover ? (_jsx(MediaImage, { src: cover, alt: bundle.name, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", className: "h-full w-full", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), showSavingsBadge && discount && (_jsx(PricesOnly, { children: _jsx(Badge, { variant: "success", className: "absolute left-2 top-2", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }) }))] }), _jsx(Text, { className: "line-clamp-2", size: "sm", weight: "semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", wrap: true, children: [_jsx(Text, { size: "sm", weight: "bold", children: _jsx(GatedPrice, { label: "Sign in to see the bundle price", children: bundle.bundlePrice
42
42
  ? formatCurrency(bundle.bundlePrice, "INR")
43
43
  : BUNDLE_COPY.featured.priceFallback }) }), discount && (_jsx(PricesOnly, { children: _jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, "INR") }) })), _jsxs(Text, { size: "xs", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.featured.itemCount(memberCount)] }), stock !== "in_stock" && (_jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: BUNDLE_COPY.stockBadge.listVariantOutOfStock }))] })] }), onBuyNow && (_jsx(Div, { border: "subtle", className: `border-t ${__P.p3}`, padding: "t-xs", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
44
44
  }
@@ -10,7 +10,7 @@ const CLS_BUNDLE_PILL = "inline-flex items-center rounded-full bg-violet-600 px-
10
10
  const CLS_STOCK_OK = "bg-success-solid text-success-on-solid";
11
11
  // audit-content-alignment-ok: button label text, not page content — buttons are universally center-labeled
12
12
  const CLS_VIEW_BTN = "mt-2 w-full cursor-pointer rounded-md bg-violet-600 py-[var(--appkit-space-1-5)] text-center text-[length:var(--appkit-text-xs)] font-semibold text-white transition-colors hover:bg-violet-700 active:scale-[0.98]";
13
- import { BaseListingCard, Div, GatedPrice, Row, Span, Stack, Text, TextLink } from "../../../ui";
13
+ import { BaseListingCard, Div, GatedPrice, PricesOnly, Row, Span, Stack, Text, TextLink } from "../../../ui";
14
14
  import { MediaImage } from "../../media/MediaImage";
15
15
  import { computeBundleDiscount } from "../../../_internal/shared/features/categories/bundle-pricing";
16
16
  import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy";
@@ -49,7 +49,7 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
49
49
  }
50
50
  router.push(String(detailHref));
51
51
  }, [detailHref, onNavigate, router]);
52
- return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), discount && (_jsx(Span, { size: "xs", weight: "bold", className: "absolute left-2 top-2 bg-success-solid text-success-on-solid", rounded: "full", padding: "pill-xs", children: BUNDLE_COPY.detail.discountBadge(discount.percent) })), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
52
+ return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), discount && (_jsx(PricesOnly, { children: _jsx(Span, { size: "xs", weight: "bold", className: "absolute left-2 top-2 bg-success-solid text-success-on-solid", rounded: "full", padding: "pill-xs", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }) })), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
53
53
  event.stopPropagation();
54
54
  onSelect(bundle.id, !isSelected);
55
55
  }, className: selectable || isSelected
@@ -162,8 +162,17 @@ function stringMaxLength(schema) {
162
162
  */
163
163
  function enumOptions(schema) {
164
164
  const direct = schema.options;
165
- if (Array.isArray(direct))
166
- return direct.filter((o) => typeof o === "string");
165
+ if (Array.isArray(direct)) {
166
+ const strings = direct.filter((o) => typeof o === "string");
167
+ // On a ZodUnion `.options` is the member SCHEMAS, not strings, so the
168
+ // filter above yields []. Empty is truthy downstream, which is how a union
169
+ // rendered as a <select> with nothing in it. Read the literals instead.
170
+ if (strings.length === 0) {
171
+ const literals = unionStringLiterals(direct);
172
+ return literals.length > 0 ? literals : undefined;
173
+ }
174
+ return strings;
175
+ }
167
176
  const values = schema._def?.values;
168
177
  if (Array.isArray(values))
169
178
  return values.filter((o) => typeof o === "string");
@@ -172,6 +181,26 @@ function enumOptions(schema) {
172
181
  }
173
182
  return undefined;
174
183
  }
184
+ /**
185
+ * String literal values carried by a union's members.
186
+ *
187
+ * `z.literal("a").or(z.literal("b"))` is a genuine choice and yields
188
+ * `["a", "b"]`. `z.string().optional().or(z.literal(""))` — the idiomatic
189
+ * "optional text that also accepts empty" — yields nothing usable, because its
190
+ * only literal is the empty string. That distinction is what decides whether a
191
+ * union field is a select or a text input.
192
+ */
193
+ function unionStringLiterals(members) {
194
+ const out = [];
195
+ for (const member of members) {
196
+ const direct = member?.value;
197
+ const nested = member?._def?.value;
198
+ const value = typeof direct === "string" ? direct : nested;
199
+ if (typeof value === "string" && value !== "")
200
+ out.push(value);
201
+ }
202
+ return out;
203
+ }
175
204
  /** Public: the option list for a select-shaped field, or null. */
176
205
  export function schemaEnumOptions(schema) {
177
206
  const { schema: inner } = unwrapSchema(schema);
@@ -180,7 +209,27 @@ export function schemaEnumOptions(schema) {
180
209
  /** Public: whether a field's type is optional-wrapped (i.e. not required). */
181
210
  export function schemaIsOptional(schema) {
182
211
  const type = typeOf(schema);
183
- return type === "optional" || type === "default" || type === "nullish";
212
+ if (type === "optional" || type === "default" || type === "nullish")
213
+ return true;
214
+ // A union HIDES optionality from the check above: the outer type is "union",
215
+ // so `z.string().optional().or(z.literal(""))` read as required and rendered
216
+ // a `*` on a field the schema is happy to leave empty. If any member is
217
+ // optional, or accepts "", the field is optional.
218
+ if (type === "union") {
219
+ const members = schema.options;
220
+ if (Array.isArray(members)) {
221
+ return members.some((member) => {
222
+ const memberType = typeOf(member);
223
+ if (memberType === "optional" || memberType === "default" || memberType === "nullish") {
224
+ return true;
225
+ }
226
+ const direct = member?.value;
227
+ const nested = member?._def?.value;
228
+ return direct === "" || nested === "";
229
+ });
230
+ }
231
+ }
232
+ return false;
184
233
  }
185
234
  /**
186
235
  * The `.shape` of an object schema, seeing through effects wrappers.
@@ -257,8 +306,19 @@ function fromZodType(type, inner) {
257
306
  case "enum":
258
307
  case "nativeEnum":
259
308
  case "literal":
260
- case "union":
261
309
  return { kind: "select", row: "pair" };
310
+ case "union": {
311
+ // Only a union that actually yields choices is a select. The other
312
+ // common shape, `z.string().optional().or(z.literal(""))`, is an optional
313
+ // TEXT field — rendering it as a select produced a dropdown with zero
314
+ // options, which is unusable and (being marked required) made the whole
315
+ // form unsubmittable. Seen live on /user/addresses/new, where
316
+ // "Address line 2" and "Landmark" were both required empty dropdowns.
317
+ const choices = enumOptions(inner);
318
+ return choices && choices.length > 0
319
+ ? { kind: "select", row: "pair" }
320
+ : { kind: "text", row: "pair" };
321
+ }
262
322
  case "date":
263
323
  return { kind: "date", row: "pair" };
264
324
  case "number":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.41.2",
3
+ "version": "4.41.4",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"