@mohasinac/appkit 4.21.0 → 4.23.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/server/features/checkout/locked-lines.js +12 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +5 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +10 -4
- package/dist/_internal/server/jobs/core/offerExpiry.js +10 -2
- package/dist/_internal/server/jobs/core/payoutBatch.js +15 -3
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/types.d.ts +16 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +4 -0
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/features/account/components/AddressForm.js +45 -29
- package/dist/features/admin/actions/admin-actions.js +13 -3
- package/dist/features/admin/components/AdminBidsView.js +7 -1
- package/dist/features/admin/components/AdminBundleEditorView.js +3 -0
- package/dist/features/admin/components/AdminScammerEditorView.d.ts +5 -1
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -2
- package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +5 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +3 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +3 -1
- package/dist/features/auctions/repository/bid.repository.d.ts +21 -8
- package/dist/features/auctions/repository/bid.repository.js +57 -33
- package/dist/features/auctions/schemas/firestore.d.ts +27 -0
- package/dist/features/auctions/schemas/firestore.js +21 -0
- package/dist/features/auth/schemas/index.d.ts +2 -2
- package/dist/features/catalogue/actions/approve-catalogue-listing.js +7 -6
- package/dist/features/catalogue/actions/list-from-catalogue.js +2 -2
- package/dist/features/catalogue/actions/submit-for-approval.js +1 -4
- package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +5 -1
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -1
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +23 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +28 -1
- package/dist/features/catalogue/schemas/firestore.d.ts +19 -0
- package/dist/features/catalogue/schemas/firestore.js +12 -0
- package/dist/features/categories/schemas/index.d.ts +2 -2
- package/dist/features/layout/BottomActions.js +18 -1
- package/dist/features/layout/BottomActionsContext.d.ts +19 -0
- package/dist/features/layout/BottomActionsContext.js +37 -0
- package/dist/features/layout/hooks/useBottomActions.d.ts +10 -0
- package/dist/features/layout/hooks/useBottomActions.js +48 -11
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +57 -0
- package/dist/features/layout/hooks/useFormBottomActions.js +89 -0
- package/dist/features/layout/index.d.ts +2 -0
- package/dist/features/layout/index.js +1 -0
- package/dist/features/messages/schemas/index.d.ts +6 -6
- package/dist/features/orders/schemas/index.d.ts +12 -12
- package/dist/features/payments/repository/payout.repository.d.ts +18 -5
- package/dist/features/payments/repository/payout.repository.js +45 -19
- package/dist/features/payments/schemas/firestore.d.ts +33 -1
- package/dist/features/payments/schemas/firestore.js +39 -0
- package/dist/features/payments/schemas/index.d.ts +12 -12
- package/dist/features/products/components/MakeOfferButton.js +5 -3
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/promotions/schemas/index.d.ts +12 -12
- package/dist/features/scams/repository/scammer.repository.d.ts +18 -1
- package/dist/features/scams/repository/scammer.repository.js +24 -2
- package/dist/features/scams/schemas/firestore.d.ts +24 -0
- package/dist/features/scams/schemas/firestore.js +30 -0
- package/dist/features/shell/QuickFormDrawer.js +15 -1
- package/dist/features/shell/SectionForm.d.ts +16 -2
- package/dist/features/shell/SectionForm.js +28 -2
- package/dist/features/shipments/components/AdminShipmentEditorView.js +5 -2
- package/dist/features/shipments/repository/shipments.repository.d.ts +18 -0
- package/dist/features/shipments/repository/shipments.repository.js +23 -1
- package/dist/features/shipments/schemas/firestore.d.ts +14 -0
- package/dist/features/shipments/schemas/firestore.js +19 -0
- package/dist/features/status-history/components/RecordStatusTimeline.d.ts +12 -0
- package/dist/features/status-history/components/RecordStatusTimeline.js +90 -0
- package/dist/features/status-history/index.d.ts +2 -0
- package/dist/features/status-history/index.js +1 -0
- package/dist/features/stores/repository/store.repository.d.ts +30 -2
- package/dist/features/stores/repository/store.repository.js +43 -4
- package/dist/features/stores/schemas/firestore.d.ts +29 -0
- package/dist/features/stores/schemas/firestore.js +31 -0
- package/dist/features/support/repository/support.repository.d.ts +47 -3
- package/dist/features/support/repository/support.repository.js +72 -12
- package/dist/features/support/schemas/firestore.d.ts +38 -1
- package/dist/features/support/schemas/firestore.js +34 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +90 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +16 -1
- package/dist/schemas/registry.d.ts +32 -32
- package/dist/seed/bids-seed-data.js +8 -0
- package/dist/ui/components/Form.d.ts +22 -1
- package/dist/ui/components/Form.js +30 -2
- package/dist/ui/components/Modal.js +4 -3
- package/dist/ui/components/SideDrawer.js +5 -4
- package/dist/ui/components/overlay-context.d.ts +21 -0
- package/dist/ui/components/overlay-context.js +25 -0
- package/dist/ui/forms/FormErrorList.d.ts +29 -0
- package/dist/ui/forms/FormErrorList.js +36 -0
- package/dist/ui/forms/FormErrorSummary.d.ts +8 -0
- package/dist/ui/forms/FormErrorSummary.js +15 -8
- package/dist/ui/forms/FormShell.d.ts +25 -1
- package/dist/ui/forms/FormShell.js +39 -4
- package/dist/ui/forms/index.d.ts +2 -0
- package/dist/ui/forms/index.js +1 -0
- package/package.json +1 -1
|
@@ -114,7 +114,7 @@ export default function BottomActions() {
|
|
|
114
114
|
setPickerOpen(false);
|
|
115
115
|
setInfoOpen(false);
|
|
116
116
|
});
|
|
117
|
-
const { actions, bulk, infoLabel, secondaryLabel, infoPanel, desktop } = state;
|
|
117
|
+
const { actions, bulk, infoLabel, secondaryLabel, infoPanel, infoOpenSignal, desktop } = state;
|
|
118
118
|
// Desktop reveal-on-scroll. Kept as a plain passive listener to match BackToTop —
|
|
119
119
|
// there is no shared scroll-position hook in the codebase and one consumer doesn't
|
|
120
120
|
// justify inventing one. Only armed when a page actually opts into "after-scroll".
|
|
@@ -144,6 +144,23 @@ export default function BottomActions() {
|
|
|
144
144
|
if (!hasInfoPanel)
|
|
145
145
|
setInfoOpen(false);
|
|
146
146
|
}, [hasInfoPanel]);
|
|
147
|
+
/*
|
|
148
|
+
* Force-open on a bumped signal — a failed form submit, today. Depends on
|
|
149
|
+
* the counter alone, so a re-render with the same value never re-opens what
|
|
150
|
+
* the user has since collapsed. `hasInfoPanel` is read but deliberately NOT
|
|
151
|
+
* a dependency: publishing the panel and bumping the signal are two separate
|
|
152
|
+
* setState calls, so re-running on `hasInfoPanel` would open the panel again
|
|
153
|
+
* one render after the user closed it.
|
|
154
|
+
*/
|
|
155
|
+
const lastSignalRef = useRef(undefined);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (infoOpenSignal == null)
|
|
158
|
+
return;
|
|
159
|
+
if (lastSignalRef.current === infoOpenSignal)
|
|
160
|
+
return;
|
|
161
|
+
lastSignalRef.current = infoOpenSignal;
|
|
162
|
+
setInfoOpen(true);
|
|
163
|
+
}, [infoOpenSignal]);
|
|
147
164
|
// Keep selectedActionId in sync with available bulk actions
|
|
148
165
|
useEffect(() => {
|
|
149
166
|
if (!isBulkMode) {
|
|
@@ -109,6 +109,19 @@ export interface BottomActionsState {
|
|
|
109
109
|
* bulk mode, which owns the same space for its action picker.
|
|
110
110
|
*/
|
|
111
111
|
infoPanel?: React.ReactNode;
|
|
112
|
+
/**
|
|
113
|
+
* A COUNTER, not a boolean — bump it to force `infoPanel` open once.
|
|
114
|
+
*
|
|
115
|
+
* `BottomActions` owns `infoOpen` in local state so the user can collapse
|
|
116
|
+
* the panel at will. A boolean `forceOpen` would fight that: it stays true,
|
|
117
|
+
* so every subsequent render re-opens what they just closed. A counter
|
|
118
|
+
* expresses the real intent — "this is a NEW event, show it" — and fires
|
|
119
|
+
* exactly once per increment.
|
|
120
|
+
*
|
|
121
|
+
* Used by a form's failed submit: the error list should appear without a
|
|
122
|
+
* tap, and stay dismissible afterwards.
|
|
123
|
+
*/
|
|
124
|
+
infoOpenSignal?: number;
|
|
112
125
|
}
|
|
113
126
|
interface BottomActionsContextValue {
|
|
114
127
|
state: BottomActionsState;
|
|
@@ -131,10 +144,16 @@ interface BottomActionsContextValue {
|
|
|
131
144
|
setSecondaryLabel: (label: string | undefined) => void;
|
|
132
145
|
/** Set or clear the expandable panel revealed above the bar. */
|
|
133
146
|
setInfoPanel: (panel: React.ReactNode | undefined) => void;
|
|
147
|
+
/** Set the force-open counter. See BottomActionsState.infoOpenSignal. */
|
|
148
|
+
setInfoOpenSignal: (signal: number | undefined) => void;
|
|
134
149
|
/** Set the desktop visibility policy. */
|
|
135
150
|
setDesktopMode: (mode: BottomActionsDesktopMode | undefined) => void;
|
|
136
151
|
/** Clear all state (called on feature unmount). */
|
|
137
152
|
clearAll: () => void;
|
|
153
|
+
claim: (id: string) => void;
|
|
154
|
+
release: (id: string) => void;
|
|
155
|
+
/** The id currently allowed to publish. `useBottomActions` gates on this. */
|
|
156
|
+
topClaimId: string | null;
|
|
138
157
|
}
|
|
139
158
|
export declare function BottomActionsProvider({ children, }: {
|
|
140
159
|
children: React.ReactNode;
|
|
@@ -47,6 +47,32 @@ export function BottomActionsProvider({ children, }) {
|
|
|
47
47
|
const actionCallbacksRef = useRef(new Map());
|
|
48
48
|
const bulkCallbacksRef = useRef(new Map());
|
|
49
49
|
const bulkClearRef = useRef(undefined);
|
|
50
|
+
// Stack of claimant ids, oldest first. The last entry owns the bar.
|
|
51
|
+
const claimsRef = useRef([]);
|
|
52
|
+
const [topClaimId, setTopClaimId] = useState(null);
|
|
53
|
+
const claim = useCallback((id) => {
|
|
54
|
+
if (claimsRef.current.includes(id))
|
|
55
|
+
return;
|
|
56
|
+
claimsRef.current = [...claimsRef.current, id];
|
|
57
|
+
setTopClaimId(id);
|
|
58
|
+
}, []);
|
|
59
|
+
const release = useCallback((id) => {
|
|
60
|
+
const next = claimsRef.current.filter((c) => c !== id);
|
|
61
|
+
if (next.length === claimsRef.current.length)
|
|
62
|
+
return;
|
|
63
|
+
claimsRef.current = next;
|
|
64
|
+
const newTop = next.length > 0 ? next[next.length - 1] : null;
|
|
65
|
+
setTopClaimId(newTop);
|
|
66
|
+
// Only blank the bar when nobody is left. Otherwise the newly-top
|
|
67
|
+
// consumer republishes on the very next effect pass, so clearing here
|
|
68
|
+
// would be a visible flicker of an empty bar.
|
|
69
|
+
if (newTop === null) {
|
|
70
|
+
actionCallbacksRef.current = new Map();
|
|
71
|
+
bulkCallbacksRef.current = new Map();
|
|
72
|
+
bulkClearRef.current = undefined;
|
|
73
|
+
setState(EMPTY);
|
|
74
|
+
}
|
|
75
|
+
}, []);
|
|
50
76
|
const setActions = useCallback((actions) => {
|
|
51
77
|
// Update callback map (always latest)
|
|
52
78
|
actionCallbacksRef.current = new Map(actions.map((a) => [a.id, a.onClick]));
|
|
@@ -90,6 +116,9 @@ export function BottomActionsProvider({ children, }) {
|
|
|
90
116
|
const setInfoPanel = useCallback((infoPanel) => {
|
|
91
117
|
setState((prev) => ({ ...prev, infoPanel }));
|
|
92
118
|
}, []);
|
|
119
|
+
const setInfoOpenSignal = useCallback((infoOpenSignal) => {
|
|
120
|
+
setState((prev) => (prev.infoOpenSignal === infoOpenSignal ? prev : { ...prev, infoOpenSignal }));
|
|
121
|
+
}, []);
|
|
93
122
|
const setDesktopMode = useCallback((desktop) => {
|
|
94
123
|
setState((prev) => (prev.desktop === desktop ? prev : { ...prev, desktop }));
|
|
95
124
|
}, []);
|
|
@@ -109,8 +138,12 @@ export function BottomActionsProvider({ children, }) {
|
|
|
109
138
|
setInfoLabel,
|
|
110
139
|
setSecondaryLabel,
|
|
111
140
|
setInfoPanel,
|
|
141
|
+
setInfoOpenSignal,
|
|
112
142
|
setDesktopMode,
|
|
113
143
|
clearAll,
|
|
144
|
+
claim,
|
|
145
|
+
release,
|
|
146
|
+
topClaimId,
|
|
114
147
|
}), [
|
|
115
148
|
state,
|
|
116
149
|
setActions,
|
|
@@ -118,8 +151,12 @@ export function BottomActionsProvider({ children, }) {
|
|
|
118
151
|
setInfoLabel,
|
|
119
152
|
setSecondaryLabel,
|
|
120
153
|
setInfoPanel,
|
|
154
|
+
setInfoOpenSignal,
|
|
121
155
|
setDesktopMode,
|
|
122
156
|
clearAll,
|
|
157
|
+
claim,
|
|
158
|
+
release,
|
|
159
|
+
topClaimId,
|
|
123
160
|
]);
|
|
124
161
|
return (_jsx(BottomActionsContext.Provider, { value: value, children: children }));
|
|
125
162
|
}
|
|
@@ -11,10 +11,20 @@ export interface UseBottomActionsOptions {
|
|
|
11
11
|
* See BottomActionsState.infoPanel.
|
|
12
12
|
*/
|
|
13
13
|
infoPanel?: React.ReactNode;
|
|
14
|
+
/** Bump to force `infoPanel` open once. See BottomActionsState.infoOpenSignal. */
|
|
15
|
+
infoOpenSignal?: number;
|
|
14
16
|
/**
|
|
15
17
|
* Whether the bar may also show on desktop. Defaults to "hidden" (mobile/tablet only),
|
|
16
18
|
* so pages that don't opt in behave exactly as before.
|
|
17
19
|
*/
|
|
18
20
|
desktop?: BottomActionsDesktopMode;
|
|
21
|
+
/**
|
|
22
|
+
* Set false to opt out entirely — no claim is taken, so this consumer is
|
|
23
|
+
* invisible to the stack and cannot displace whoever currently owns the
|
|
24
|
+
* bar. Load-bearing: `useFormBottomActions` is called unconditionally from
|
|
25
|
+
* `<Form>`, which mounts on ~75 surfaces, and a claim from each of those
|
|
26
|
+
* would push a listing's bulk bar off the stack for nothing.
|
|
27
|
+
*/
|
|
28
|
+
enabled?: boolean;
|
|
19
29
|
}
|
|
20
30
|
export declare function useBottomActions(options?: UseBottomActionsOptions): void;
|
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useEffect, useId, useRef } from "react";
|
|
3
3
|
import { useBottomActionsContext, } from "../BottomActionsContext";
|
|
4
4
|
export function useBottomActions(options = {}) {
|
|
5
|
-
const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, setInfoPanel, setDesktopMode,
|
|
5
|
+
const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, setInfoPanel, setInfoOpenSignal, setDesktopMode, claim, release, topClaimId, } = useBottomActionsContext();
|
|
6
|
+
/*
|
|
7
|
+
* One bar, many potential claimants. `DataListingView` already holds it on
|
|
8
|
+
* ~70 admin screens, so a form opened in a drawer over a listing used to
|
|
9
|
+
* overwrite the listing's bar and blank it on close.
|
|
10
|
+
*
|
|
11
|
+
* Claim on mount, release on unmount. `isTop` gates every publish below, so
|
|
12
|
+
* a backgrounded consumer stops writing — and because `isTop` is a
|
|
13
|
+
* dependency of each effect, regaining the top re-publishes everything
|
|
14
|
+
* automatically. That is what restores the listing's bar when the form
|
|
15
|
+
* closes, with no cooperation needed from either side.
|
|
16
|
+
*/
|
|
17
|
+
const enabled = options.enabled !== false;
|
|
18
|
+
const claimId = useId();
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!enabled)
|
|
21
|
+
return;
|
|
22
|
+
claim(claimId);
|
|
23
|
+
return () => release(claimId);
|
|
24
|
+
}, [claim, release, claimId, enabled]);
|
|
25
|
+
const isTop = enabled && topClaimId === claimId;
|
|
6
26
|
const actionsRef = useRef(options.actions ?? []);
|
|
7
27
|
actionsRef.current = options.actions ?? [];
|
|
8
28
|
const bulkRef = useRef(options.bulk);
|
|
@@ -14,17 +34,25 @@ export function useBottomActions(options = {}) {
|
|
|
14
34
|
? `${options.bulk.selectedCount}|${options.bulk.actions.map((a) => `${a.id}|${a.label}|${a.variant}|${a.badge}|${a.disabled}|${a.loading}|${a.grow}`).join(",")}`
|
|
15
35
|
: "";
|
|
16
36
|
useEffect(() => {
|
|
37
|
+
if (!isTop)
|
|
38
|
+
return;
|
|
17
39
|
setActions(actionsRef.current);
|
|
18
|
-
}, [setActions, actionKey]);
|
|
40
|
+
}, [setActions, actionKey, isTop]);
|
|
19
41
|
useEffect(() => {
|
|
42
|
+
if (!isTop)
|
|
43
|
+
return;
|
|
20
44
|
setBulkConfig(bulkRef.current);
|
|
21
|
-
}, [setBulkConfig, bulkCountKey]);
|
|
45
|
+
}, [setBulkConfig, bulkCountKey, isTop]);
|
|
22
46
|
useEffect(() => {
|
|
47
|
+
if (!isTop)
|
|
48
|
+
return;
|
|
23
49
|
setInfoLabel(options.infoLabel);
|
|
24
|
-
}, [setInfoLabel, options.infoLabel]);
|
|
50
|
+
}, [setInfoLabel, options.infoLabel, isTop]);
|
|
25
51
|
useEffect(() => {
|
|
52
|
+
if (!isTop)
|
|
53
|
+
return;
|
|
26
54
|
setSecondaryLabel(options.secondaryLabel);
|
|
27
|
-
}, [setSecondaryLabel, options.secondaryLabel]);
|
|
55
|
+
}, [setSecondaryLabel, options.secondaryLabel, isTop]);
|
|
28
56
|
// A ReactNode is a fresh object on every render, so it can't be a dependency
|
|
29
57
|
// without looping. Mirror it through a ref and re-publish whenever anything
|
|
30
58
|
// the panel actually derives from changes — callers pass `infoLabel`, which
|
|
@@ -33,12 +61,21 @@ export function useBottomActions(options = {}) {
|
|
|
33
61
|
infoPanelRef.current = options.infoPanel;
|
|
34
62
|
const hasInfoPanel = options.infoPanel != null;
|
|
35
63
|
useEffect(() => {
|
|
64
|
+
if (!isTop)
|
|
65
|
+
return;
|
|
36
66
|
setInfoPanel(infoPanelRef.current);
|
|
37
|
-
}, [setInfoPanel, hasInfoPanel, options.infoLabel, options.secondaryLabel]);
|
|
67
|
+
}, [setInfoPanel, hasInfoPanel, options.infoLabel, options.secondaryLabel, isTop]);
|
|
38
68
|
useEffect(() => {
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
if (!isTop)
|
|
70
|
+
return;
|
|
71
|
+
setInfoOpenSignal(options.infoOpenSignal);
|
|
72
|
+
}, [setInfoOpenSignal, options.infoOpenSignal, isTop]);
|
|
41
73
|
useEffect(() => {
|
|
42
|
-
|
|
43
|
-
|
|
74
|
+
if (!isTop)
|
|
75
|
+
return;
|
|
76
|
+
setDesktopMode(options.desktop);
|
|
77
|
+
}, [setDesktopMode, options.desktop, isTop]);
|
|
78
|
+
// NOTE: no clearAll() on unmount any more — `release()` above owns that, and
|
|
79
|
+
// only blanks the bar when the stack empties. Clearing here would flash an
|
|
80
|
+
// empty bar between a drawer closing and the listing behind it republishing.
|
|
44
81
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type FormShellContextValue } from "../../../ui/forms/FormShell";
|
|
2
|
+
export interface UseFormBottomActionsOptions {
|
|
3
|
+
/** Runs when the pinned primary button is pressed. */
|
|
4
|
+
onSubmit: () => void | Promise<void>;
|
|
5
|
+
/** Omit to render no Cancel button — a settings page with nowhere to go back to. */
|
|
6
|
+
onCancel?: () => void;
|
|
7
|
+
submitLabel?: string;
|
|
8
|
+
cancelLabel?: string;
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** Set false to leave the bar to someone else on this route. */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Read the form state from here instead of the ambient context. `<Form>`
|
|
15
|
+
* needs it: the hook runs in `Form`'s body, which is ABOVE the provider
|
|
16
|
+
* `Form` itself renders, so the ambient lookup would find the parent's
|
|
17
|
+
* context or none at all.
|
|
18
|
+
*/
|
|
19
|
+
ctx?: FormShellContextValue;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pin a form's Save/Cancel row to the bottom of a phone screen, and turn its
|
|
23
|
+
* validation errors into a pull-up sheet above it.
|
|
24
|
+
*
|
|
25
|
+
* ## Nothing here is new chrome
|
|
26
|
+
*
|
|
27
|
+
* This is `useBottomActions` with a form-shaped payload. The cart's bottom bar
|
|
28
|
+
* — "Total ₹777 · Details ⌃" over a pinned CTA — is 100% that primitive's
|
|
29
|
+
* output already, so the sheet mechanics, the collapse rule
|
|
30
|
+
* (`grid-rows-[1fr] ↔ [0fr]`, background on the innermost div),
|
|
31
|
+
* click-outside-to-close, the `ResizeObserver` that publishes
|
|
32
|
+
* `--bottom-chrome-height`, and all three bottom-edge offsets come for free.
|
|
33
|
+
* Re-deriving any of that would be a second, worse copy.
|
|
34
|
+
*
|
|
35
|
+
* ## Three details that will bite anyone editing this
|
|
36
|
+
*
|
|
37
|
+
* 1. **The label must encode the count.** `useBottomActions` re-publishes
|
|
38
|
+
* `infoPanel` only when `hasInfoPanel`, `infoLabel` or `secondaryLabel`
|
|
39
|
+
* change — a ReactNode is a fresh object every render and cannot be a
|
|
40
|
+
* dependency without looping. "Fix 6 issues" → "Fix 5 issues" is
|
|
41
|
+
* therefore what pushes the updated list through. A generic label like
|
|
42
|
+
* "Errors" would freeze the sheet's contents at whatever they were the
|
|
43
|
+
* first time it opened.
|
|
44
|
+
*
|
|
45
|
+
* 2. **The panel is rendered outside the form's provider.** `BottomActions`
|
|
46
|
+
* lives in the layout shell, so the detached node cannot reach
|
|
47
|
+
* `FormShellContext` by itself — hence `<FormErrorList ctx={ctx}>` with
|
|
48
|
+
* the context passed explicitly.
|
|
49
|
+
*
|
|
50
|
+
* 3. **Suppressed inside a Modal or SideDrawer.** An overlay owns its own
|
|
51
|
+
* footer, and a viewport-fixed bar would render *behind* the backdrop,
|
|
52
|
+
* below the dialog it belongs to. `useIsInsideOverlay()` is how the
|
|
53
|
+
* overlay tells us, because neither portal leaves a detectable trace in
|
|
54
|
+
* the DOM at the form's position.
|
|
55
|
+
*/
|
|
56
|
+
export declare function useFormBottomActions({ onSubmit, onCancel, submitLabel, cancelLabel, isLoading, disabled, enabled, ctx: ctxProp, }: UseFormBottomActionsOptions): void;
|
|
57
|
+
export default useFormBottomActions;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useContext, useMemo } from "react";
|
|
4
|
+
import { FormShellContext } from "../../../ui/forms/FormShell";
|
|
5
|
+
import { FormErrorList } from "../../../ui/forms/FormErrorList";
|
|
6
|
+
import { useIsInsideOverlay } from "../../../ui/components/overlay-context";
|
|
7
|
+
import { useBottomActions } from "./useBottomActions";
|
|
8
|
+
/**
|
|
9
|
+
* Pin a form's Save/Cancel row to the bottom of a phone screen, and turn its
|
|
10
|
+
* validation errors into a pull-up sheet above it.
|
|
11
|
+
*
|
|
12
|
+
* ## Nothing here is new chrome
|
|
13
|
+
*
|
|
14
|
+
* This is `useBottomActions` with a form-shaped payload. The cart's bottom bar
|
|
15
|
+
* — "Total ₹777 · Details ⌃" over a pinned CTA — is 100% that primitive's
|
|
16
|
+
* output already, so the sheet mechanics, the collapse rule
|
|
17
|
+
* (`grid-rows-[1fr] ↔ [0fr]`, background on the innermost div),
|
|
18
|
+
* click-outside-to-close, the `ResizeObserver` that publishes
|
|
19
|
+
* `--bottom-chrome-height`, and all three bottom-edge offsets come for free.
|
|
20
|
+
* Re-deriving any of that would be a second, worse copy.
|
|
21
|
+
*
|
|
22
|
+
* ## Three details that will bite anyone editing this
|
|
23
|
+
*
|
|
24
|
+
* 1. **The label must encode the count.** `useBottomActions` re-publishes
|
|
25
|
+
* `infoPanel` only when `hasInfoPanel`, `infoLabel` or `secondaryLabel`
|
|
26
|
+
* change — a ReactNode is a fresh object every render and cannot be a
|
|
27
|
+
* dependency without looping. "Fix 6 issues" → "Fix 5 issues" is
|
|
28
|
+
* therefore what pushes the updated list through. A generic label like
|
|
29
|
+
* "Errors" would freeze the sheet's contents at whatever they were the
|
|
30
|
+
* first time it opened.
|
|
31
|
+
*
|
|
32
|
+
* 2. **The panel is rendered outside the form's provider.** `BottomActions`
|
|
33
|
+
* lives in the layout shell, so the detached node cannot reach
|
|
34
|
+
* `FormShellContext` by itself — hence `<FormErrorList ctx={ctx}>` with
|
|
35
|
+
* the context passed explicitly.
|
|
36
|
+
*
|
|
37
|
+
* 3. **Suppressed inside a Modal or SideDrawer.** An overlay owns its own
|
|
38
|
+
* footer, and a viewport-fixed bar would render *behind* the backdrop,
|
|
39
|
+
* below the dialog it belongs to. `useIsInsideOverlay()` is how the
|
|
40
|
+
* overlay tells us, because neither portal leaves a detectable trace in
|
|
41
|
+
* the DOM at the form's position.
|
|
42
|
+
*/
|
|
43
|
+
export function useFormBottomActions({ onSubmit, onCancel, submitLabel = "Save", cancelLabel = "Cancel", isLoading = false, disabled = false, enabled = true, ctx: ctxProp, }) {
|
|
44
|
+
const ambient = useContext(FormShellContext);
|
|
45
|
+
const ctx = ctxProp ?? ambient;
|
|
46
|
+
const insideOverlay = useIsInsideOverlay();
|
|
47
|
+
const active = enabled && !insideOverlay && ctx != null;
|
|
48
|
+
const errorCount = ctx ? Object.keys(ctx.errors).length : 0;
|
|
49
|
+
const showErrors = !!ctx?.submitAttempted && errorCount > 0;
|
|
50
|
+
const actions = useMemo(() => {
|
|
51
|
+
if (!active)
|
|
52
|
+
return [];
|
|
53
|
+
const list = [];
|
|
54
|
+
if (onCancel) {
|
|
55
|
+
list.push({
|
|
56
|
+
id: "form-cancel",
|
|
57
|
+
label: cancelLabel,
|
|
58
|
+
variant: "outline",
|
|
59
|
+
grow: false,
|
|
60
|
+
onClick: onCancel,
|
|
61
|
+
disabled: isLoading,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
list.push({
|
|
65
|
+
id: "form-submit",
|
|
66
|
+
label: submitLabel,
|
|
67
|
+
variant: "primary",
|
|
68
|
+
grow: true,
|
|
69
|
+
loading: isLoading,
|
|
70
|
+
disabled: disabled || isLoading,
|
|
71
|
+
onClick: () => void onSubmit(),
|
|
72
|
+
});
|
|
73
|
+
return list;
|
|
74
|
+
}, [active, onCancel, cancelLabel, submitLabel, isLoading, disabled, onSubmit]);
|
|
75
|
+
useBottomActions({
|
|
76
|
+
actions,
|
|
77
|
+
// Encodes the count on purpose — see (1) in the header.
|
|
78
|
+
infoLabel: showErrors
|
|
79
|
+
? `Fix ${errorCount} ${errorCount === 1 ? "issue" : "issues"}`
|
|
80
|
+
: undefined,
|
|
81
|
+
infoPanel: showErrors && ctx ? _jsx(FormErrorList, { ctx: ctx }) : undefined,
|
|
82
|
+
// Bumped by every submit attempt, so a second failed Save re-opens a
|
|
83
|
+
// sheet the user collapsed — while an ordinary re-render does not.
|
|
84
|
+
infoOpenSignal: showErrors ? ctx?.submitAttemptCount : undefined,
|
|
85
|
+
// No claim at all when inactive — see UseBottomActionsOptions.enabled.
|
|
86
|
+
enabled: active,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export default useFormBottomActions;
|
|
@@ -30,6 +30,8 @@ export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActions
|
|
|
30
30
|
export type { BottomAction, BottomBulkConfig, BottomActionsState, BottomActionsDesktopMode, } from "./BottomActionsContext";
|
|
31
31
|
export { useBottomActions } from "./hooks/useBottomActions";
|
|
32
32
|
export type { UseBottomActionsOptions } from "./hooks/useBottomActions";
|
|
33
|
+
export { useFormBottomActions } from "./hooks/useFormBottomActions";
|
|
34
|
+
export type { UseFormBottomActionsOptions } from "./hooks/useFormBottomActions";
|
|
33
35
|
export { default as BottomActions } from "./BottomActions";
|
|
34
36
|
export { BottomChrome } from "./BottomChrome";
|
|
35
37
|
export type { BottomChromeProps } from "./BottomChrome";
|
|
@@ -16,6 +16,7 @@ export { LayoutClient } from "./LayoutClient";
|
|
|
16
16
|
export { AppLayoutShell } from "./AppLayoutShell";
|
|
17
17
|
export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActionsContext";
|
|
18
18
|
export { useBottomActions } from "./hooks/useBottomActions";
|
|
19
|
+
export { useFormBottomActions } from "./hooks/useFormBottomActions";
|
|
19
20
|
export { default as BottomActions } from "./BottomActions";
|
|
20
21
|
export { BottomChrome } from "./BottomChrome";
|
|
21
22
|
export { BottomNavbar } from "./BottomNavbar";
|
|
@@ -21,7 +21,7 @@ export declare const conversationMessageSchema: z.ZodObject<{
|
|
|
21
21
|
body: string;
|
|
22
22
|
isRead: boolean;
|
|
23
23
|
senderId: string;
|
|
24
|
-
senderRole: "
|
|
24
|
+
senderRole: "buyer" | "seller";
|
|
25
25
|
sentAt: string | Date | z.objectOutputType<{
|
|
26
26
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
27
27
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -31,7 +31,7 @@ export declare const conversationMessageSchema: z.ZodObject<{
|
|
|
31
31
|
body: string;
|
|
32
32
|
isRead: boolean;
|
|
33
33
|
senderId: string;
|
|
34
|
-
senderRole: "
|
|
34
|
+
senderRole: "buyer" | "seller";
|
|
35
35
|
sentAt: string | Date | z.objectInputType<{
|
|
36
36
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
37
37
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -79,7 +79,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
|
|
|
79
79
|
body: string;
|
|
80
80
|
isRead: boolean;
|
|
81
81
|
senderId: string;
|
|
82
|
-
senderRole: "
|
|
82
|
+
senderRole: "buyer" | "seller";
|
|
83
83
|
sentAt: string | Date | z.objectOutputType<{
|
|
84
84
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
85
85
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -89,7 +89,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
|
|
|
89
89
|
body: string;
|
|
90
90
|
isRead: boolean;
|
|
91
91
|
senderId: string;
|
|
92
|
-
senderRole: "
|
|
92
|
+
senderRole: "buyer" | "seller";
|
|
93
93
|
sentAt: string | Date | z.objectInputType<{
|
|
94
94
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
95
95
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -127,7 +127,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
|
|
|
127
127
|
body: string;
|
|
128
128
|
isRead: boolean;
|
|
129
129
|
senderId: string;
|
|
130
|
-
senderRole: "
|
|
130
|
+
senderRole: "buyer" | "seller";
|
|
131
131
|
sentAt: string | Date | z.objectOutputType<{
|
|
132
132
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
133
133
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -160,7 +160,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
|
|
|
160
160
|
body: string;
|
|
161
161
|
isRead: boolean;
|
|
162
162
|
senderId: string;
|
|
163
|
-
senderRole: "
|
|
163
|
+
senderRole: "buyer" | "seller";
|
|
164
164
|
sentAt: string | Date | z.objectInputType<{
|
|
165
165
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
166
166
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -128,9 +128,9 @@ export declare const orderRefundEventSchema: z.ZodObject<{
|
|
|
128
128
|
overrideReason: z.ZodOptional<z.ZodString>;
|
|
129
129
|
}, "strip", z.ZodTypeAny, {
|
|
130
130
|
type: "full" | "partial";
|
|
131
|
+
reason: string;
|
|
131
132
|
amount: number;
|
|
132
133
|
refundId: string;
|
|
133
|
-
reason: string;
|
|
134
134
|
refundedAt: string | Date | z.objectOutputType<{
|
|
135
135
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
136
136
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -143,9 +143,9 @@ export declare const orderRefundEventSchema: z.ZodObject<{
|
|
|
143
143
|
overrideReason?: string | undefined;
|
|
144
144
|
}, {
|
|
145
145
|
type: "full" | "partial";
|
|
146
|
+
reason: string;
|
|
146
147
|
amount: number;
|
|
147
148
|
refundId: string;
|
|
148
|
-
reason: string;
|
|
149
149
|
refundedAt: string | Date | z.objectInputType<{
|
|
150
150
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
151
151
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -168,14 +168,14 @@ export declare const appliedOrderDiscountSchema: z.ZodObject<{
|
|
|
168
168
|
code: string;
|
|
169
169
|
type: "auto" | "coupon" | "deal";
|
|
170
170
|
discountAmount: number;
|
|
171
|
-
scope?: "
|
|
171
|
+
scope?: "seller" | "admin" | undefined;
|
|
172
172
|
storeId?: string | undefined;
|
|
173
173
|
couponId?: string | undefined;
|
|
174
174
|
}, {
|
|
175
175
|
code: string;
|
|
176
176
|
type: "auto" | "coupon" | "deal";
|
|
177
177
|
discountAmount: number;
|
|
178
|
-
scope?: "
|
|
178
|
+
scope?: "seller" | "admin" | undefined;
|
|
179
179
|
storeId?: string | undefined;
|
|
180
180
|
couponId?: string | undefined;
|
|
181
181
|
}>;
|
|
@@ -326,14 +326,14 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
326
326
|
code: string;
|
|
327
327
|
type: "auto" | "coupon" | "deal";
|
|
328
328
|
discountAmount: number;
|
|
329
|
-
scope?: "
|
|
329
|
+
scope?: "seller" | "admin" | undefined;
|
|
330
330
|
storeId?: string | undefined;
|
|
331
331
|
couponId?: string | undefined;
|
|
332
332
|
}, {
|
|
333
333
|
code: string;
|
|
334
334
|
type: "auto" | "coupon" | "deal";
|
|
335
335
|
discountAmount: number;
|
|
336
|
-
scope?: "
|
|
336
|
+
scope?: "seller" | "admin" | undefined;
|
|
337
337
|
storeId?: string | undefined;
|
|
338
338
|
couponId?: string | undefined;
|
|
339
339
|
}>, "many">>;
|
|
@@ -464,9 +464,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
464
464
|
overrideReason: z.ZodOptional<z.ZodString>;
|
|
465
465
|
}, "strip", z.ZodTypeAny, {
|
|
466
466
|
type: "full" | "partial";
|
|
467
|
+
reason: string;
|
|
467
468
|
amount: number;
|
|
468
469
|
refundId: string;
|
|
469
|
-
reason: string;
|
|
470
470
|
refundedAt: string | Date | z.objectOutputType<{
|
|
471
471
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
472
472
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -479,9 +479,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
479
479
|
overrideReason?: string | undefined;
|
|
480
480
|
}, {
|
|
481
481
|
type: "full" | "partial";
|
|
482
|
+
reason: string;
|
|
482
483
|
amount: number;
|
|
483
484
|
refundId: string;
|
|
484
|
-
reason: string;
|
|
485
485
|
refundedAt: string | Date | z.objectInputType<{
|
|
486
486
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
487
487
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -622,7 +622,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
622
622
|
code: string;
|
|
623
623
|
type: "auto" | "coupon" | "deal";
|
|
624
624
|
discountAmount: number;
|
|
625
|
-
scope?: "
|
|
625
|
+
scope?: "seller" | "admin" | undefined;
|
|
626
626
|
storeId?: string | undefined;
|
|
627
627
|
couponId?: string | undefined;
|
|
628
628
|
}[] | undefined;
|
|
@@ -654,9 +654,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
654
654
|
paymentBatchId?: string | undefined;
|
|
655
655
|
refunds?: {
|
|
656
656
|
type: "full" | "partial";
|
|
657
|
+
reason: string;
|
|
657
658
|
amount: number;
|
|
658
659
|
refundId: string;
|
|
659
|
-
reason: string;
|
|
660
660
|
refundedAt: string | Date | z.objectOutputType<{
|
|
661
661
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
662
662
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -770,7 +770,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
770
770
|
code: string;
|
|
771
771
|
type: "auto" | "coupon" | "deal";
|
|
772
772
|
discountAmount: number;
|
|
773
|
-
scope?: "
|
|
773
|
+
scope?: "seller" | "admin" | undefined;
|
|
774
774
|
storeId?: string | undefined;
|
|
775
775
|
couponId?: string | undefined;
|
|
776
776
|
}[] | undefined;
|
|
@@ -802,9 +802,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
802
802
|
paymentBatchId?: string | undefined;
|
|
803
803
|
refunds?: {
|
|
804
804
|
type: "full" | "partial";
|
|
805
|
+
reason: string;
|
|
805
806
|
amount: number;
|
|
806
807
|
refundId: string;
|
|
807
|
-
reason: string;
|
|
808
808
|
refundedAt: string | Date | z.objectInputType<{
|
|
809
809
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
810
810
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
import { BaseRepository, parseSieveDateValue, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
|
|
8
8
|
import type { DocumentReference, WriteResult } from "firebase-admin/firestore";
|
|
9
9
|
import type { PayoutDocument, PayoutCreateInput, PayoutRefundDeduction, PayoutStatus, PayoutUpdateInput } from "../schemas";
|
|
10
|
+
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
11
|
+
/** Who/why for a payout write that lands in `statusHistory`. */
|
|
12
|
+
export interface PayoutWriteContext {
|
|
13
|
+
actor?: HistoryActor;
|
|
14
|
+
trigger?: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
note?: string;
|
|
17
|
+
}
|
|
10
18
|
export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
|
|
11
19
|
constructor();
|
|
12
20
|
/** Decrypt PII fields on a payout document after Firestore read */
|
|
@@ -46,12 +54,17 @@ export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
|
|
|
46
54
|
data: PayoutDocument;
|
|
47
55
|
}>>;
|
|
48
56
|
/**
|
|
49
|
-
* Update payout status (admin action)
|
|
57
|
+
* Update payout status (admin action).
|
|
58
|
+
*
|
|
59
|
+
* `prior` keeps the timeline entry read-free — the admin route already
|
|
60
|
+
* fetches the payout to authorise the change (Rule #6).
|
|
50
61
|
*/
|
|
51
|
-
updateStatus(payoutId: string, status: PayoutStatus, extra?: PayoutUpdateInput): Promise<PayoutDocument>;
|
|
52
|
-
markProcessing(ref: DocumentReference): Promise<WriteResult>;
|
|
53
|
-
recordSuccess(ref: DocumentReference, razorpayPayoutId: string, razorpayStatus: string): Promise<WriteResult>;
|
|
54
|
-
recordFailure(ref: DocumentReference, failureCount: number, reason: string, isFinal: boolean): Promise<WriteResult>;
|
|
62
|
+
updateStatus(payoutId: string, status: PayoutStatus, extra?: PayoutUpdateInput, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<PayoutDocument>;
|
|
63
|
+
markProcessing(ref: DocumentReference, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<WriteResult>;
|
|
64
|
+
recordSuccess(ref: DocumentReference, razorpayPayoutId: string, razorpayStatus: string, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<WriteResult>;
|
|
65
|
+
recordFailure(ref: DocumentReference, failureCount: number, reason: string, isFinal: boolean, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<WriteResult>;
|
|
66
|
+
/** Append a timeline entry when the patch touches a tracked field. */
|
|
67
|
+
private withPayoutHistory;
|
|
55
68
|
/**
|
|
56
69
|
* Get all order IDs that have already been paid out for a seller.
|
|
57
70
|
* Used to avoid double-paying the same orders.
|