@base44/app-plugin-commerce 0.1.3 → 0.1.5
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/README.md +11 -11
- package/base44/agents/commerce/StoreAdmin.jsonc +7 -7
- package/base44/entities/commerce.Order.jsonc +1 -1
- package/base44/entities/commerce.PaymentGateway.jsonc +3 -3
- package/base44/entities/commerce.Product.jsonc +9 -45
- package/base44/entities/commerce.ProductAttribute.jsonc +7 -19
- package/base44/entities/commerce.ProductAttributeTerm.jsonc +4 -10
- package/base44/entities/commerce.ProductTag.jsonc +1 -8
- package/base44/entities/commerce.ProductVariation.jsonc +1 -1
- package/base44/entities/commerce.StoreSettings.jsonc +1 -1
- package/base44/functions/commerce/admin-products/entry.ts +73 -121
- package/base44/functions/commerce/admin-refunds/entry.ts +1 -1
- package/base44/functions/commerce/admin-reports/entry.ts +1 -1
- package/base44/functions/commerce/admin-tools/entry.ts +4 -3
- package/base44/functions/commerce/payments/entry.ts +0 -2
- package/base44/functions/commerce/seed-store/defaults.ts +13 -39
- package/base44/functions/commerce/seed-store/entry.ts +50 -166
- package/base44/functions/commerce/seed-store/sample-data.ts +28 -66
- package/base44/functions/commerce/seed-store/seed-catalog.ts +717 -0
- package/base44/functions/commerce/storefront-cart/entry.ts +0 -7
- package/base44/functions/commerce/storefront-catalog/entry.ts +12 -21
- package/base44/functions/commerce/storefront-checkout/entry.ts +5 -12
- package/base44/shared/commerce/catalog.ts +134 -0
- package/base44/shared/commerce/coupons.ts +4 -7
- package/base44/shared/commerce/email-templates.ts +15 -13
- package/base44/shared/commerce/emails.ts +4 -3
- package/base44/shared/commerce/payments.ts +8 -20
- package/base44/shared/commerce/products.ts +24 -0
- package/base44/shared/commerce/settings.ts +2 -3
- package/base44/shared/commerce/stock.ts +2 -4
- package/base44/shared/commerce/totals.ts +37 -42
- package/package.json +1 -1
- package/scripts/install.js +23 -7
- package/skills/commerce/SKILL.md +9 -7
- package/skills/commerce/docs/api-admin.md +20 -14
- package/skills/commerce/docs/api-storefront.md +39 -35
- package/skills/commerce/installation-guidelines.md +8 -7
- package/skills/commerce/post-installation.md +205 -84
- package/skills/commerce/references/admin-product-form.md +56 -0
- package/skills/commerce/references/emails.md +4 -3
- package/skills/commerce/references/guest-access-security.md +2 -2
- package/skills/commerce/references/limits-and-performance.md +1 -1
- package/skills/commerce/references/online-payments.md +8 -8
- package/skills/commerce/references/product-render.md +25 -23
- package/skills/commerce/references/reviews.md +20 -0
- package/skills/commerce/references/storefront-product-page.md +9 -9
- package/skills/commerce/references/webhooks.md +3 -1
- package/src/commerce/admin/README.md +4 -4
- package/src/commerce/admin/context/BasePathContext.jsx +5 -5
- package/src/commerce/admin/context/SettingsContext.jsx +4 -4
- package/src/commerce/admin/index.jsx +3 -3
- package/src/commerce/admin/layout/Sidebar.jsx +1 -9
- package/src/commerce/admin/lib/constants.js +0 -7
- package/src/commerce/admin/lib/paths.js +6 -6
- package/src/commerce/admin/lib/product-utils.js +18 -15
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +8 -8
- package/src/commerce/admin/pages/products/ProductEditor.jsx +10 -12
- package/src/commerce/admin/pages/products/ProductsList.jsx +3 -15
- package/src/commerce/admin/pages/products/components/AttributesSection.jsx +426 -0
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -69
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +1 -1
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +1 -14
- package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +19 -0
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +728 -0
- package/src/commerce/admin/pages/reports/Reports.jsx +3 -3
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +12 -14
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +6 -116
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +101 -62
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -6
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -1
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +3 -3
- package/src/commerce/admin/pages/status/Webhooks.jsx +2 -2
- package/src/commerce/admin/routes.jsx +10 -19
- package/src/commerce/utils/index.js +2 -2
- package/src/commerce/utils/shipping-promos.js +9 -6
- package/src/commerce/utils/variants.js +21 -21
- package/src/commerce/admin/pages/products/AttributeTerms.jsx +0 -180
- package/src/commerce/admin/pages/products/Attributes.jsx +0 -183
- package/src/commerce/admin/pages/products/Tags.jsx +0 -150
- package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +0 -48
- package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +0 -208
- package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +0 -41
- package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +0 -103
- package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +0 -93
- package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +0 -86
- package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +0 -377
|
@@ -28,7 +28,7 @@ import DataTable from "../../components/DataTable";
|
|
|
28
28
|
import StatusBadge from "../../components/StatusBadge";
|
|
29
29
|
import {
|
|
30
30
|
COUPON_TYPES,
|
|
31
|
-
|
|
31
|
+
PRODUCT_STATUSES,
|
|
32
32
|
REVIEW_STATUSES,
|
|
33
33
|
} from "../../lib/constants";
|
|
34
34
|
|
|
@@ -365,9 +365,9 @@ function TotalsTab() {
|
|
|
365
365
|
renderLabel={(r) => <StatusBadge status={r.key} className="text-[10px]" />}
|
|
366
366
|
/>
|
|
367
367
|
<TotalsCard
|
|
368
|
-
title="Products by
|
|
368
|
+
title="Products by status"
|
|
369
369
|
loading={loading}
|
|
370
|
-
rows={normalizeTotals(d["products-totals"],
|
|
370
|
+
rows={normalizeTotals(d["products-totals"], PRODUCT_STATUSES)}
|
|
371
371
|
/>
|
|
372
372
|
<TotalsCard title="Customers" loading={loading} rows={normalizeTotals(d["customers-totals"])} />
|
|
373
373
|
<TotalsCard
|
|
@@ -24,18 +24,16 @@ import {
|
|
|
24
24
|
import { Settings2 } from "lucide-react";
|
|
25
25
|
import { EMAIL_TYPES } from "../../lib/constants";
|
|
26
26
|
import { call } from "../../lib/api";
|
|
27
|
-
import { useSettings } from "../../context/SettingsContext";
|
|
28
27
|
import useGroupForm from "./useGroupForm";
|
|
29
28
|
|
|
30
29
|
const DEFAULTS = {
|
|
31
|
-
|
|
30
|
+
store_name: "",
|
|
32
31
|
admin_recipients: [],
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
export default function EmailsSettings() {
|
|
36
35
|
const { form, setField, SaveButton } = useGroupForm("emails", DEFAULTS);
|
|
37
|
-
const
|
|
38
|
-
const storeName = get("general", "store_name", "");
|
|
36
|
+
const storeName = form.store_name || "";
|
|
39
37
|
const [activeId, setActiveId] = useState(null);
|
|
40
38
|
// Resolved by the backend — listing admin users needs service role.
|
|
41
39
|
const [effective, setEffective] = useState(null);
|
|
@@ -46,8 +44,7 @@ export default function EmailsSettings() {
|
|
|
46
44
|
.catch(() => setEffective(null));
|
|
47
45
|
}, []);
|
|
48
46
|
|
|
49
|
-
//
|
|
50
|
-
// hint bolds the value actually in use.
|
|
47
|
+
// The fallback is the placeholder, and the hint bolds the value actually in use.
|
|
51
48
|
const adminUsers = (effective?.admin_users ?? []).join(", ");
|
|
52
49
|
|
|
53
50
|
// Per-type config lives at the top level of the `emails` group, keyed by type
|
|
@@ -75,23 +72,24 @@ export default function EmailsSettings() {
|
|
|
75
72
|
<div className="space-y-1.5">
|
|
76
73
|
<CardTitle>Sender options</CardTitle>
|
|
77
74
|
<CardDescription>
|
|
78
|
-
|
|
79
|
-
email configuration, not here.
|
|
75
|
+
How transactional emails are named, and who gets the admin copies. The sending
|
|
76
|
+
address is controlled by your Base44 email configuration, not here.
|
|
80
77
|
</CardDescription>
|
|
81
78
|
</div>
|
|
82
79
|
<SaveButton />
|
|
83
80
|
</CardHeader>
|
|
84
81
|
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
85
82
|
<div className="space-y-1.5 sm:col-span-2">
|
|
86
|
-
<Label
|
|
83
|
+
<Label>Store name</Label>
|
|
87
84
|
<Input
|
|
88
|
-
value={form.
|
|
89
|
-
placeholder=
|
|
90
|
-
onChange={(e) => setField("
|
|
85
|
+
value={form.store_name || ""}
|
|
86
|
+
placeholder="Your store's name"
|
|
87
|
+
onChange={(e) => setField("store_name", e.target.value)}
|
|
91
88
|
/>
|
|
92
89
|
<p className="text-xs text-muted-foreground">
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
Used in the subject line and as the “From” name — e.g.{" "}
|
|
91
|
+
“[{storeName || "Your store"}]: New order #1002”, sent from {storeName || "Your store"}.
|
|
92
|
+
Leave it blank to send as your app's name.
|
|
95
93
|
</p>
|
|
96
94
|
</div>
|
|
97
95
|
<div className="space-y-1.5 sm:col-span-2">
|
|
@@ -2,7 +2,6 @@ import React from "react";
|
|
|
2
2
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
3
|
import { Input } from "@/components/ui/input";
|
|
4
4
|
import { Label } from "@/components/ui/label";
|
|
5
|
-
import { Checkbox } from "@/components/ui/checkbox";
|
|
6
5
|
import {
|
|
7
6
|
Select,
|
|
8
7
|
SelectContent,
|
|
@@ -10,19 +9,11 @@ import {
|
|
|
10
9
|
SelectTrigger,
|
|
11
10
|
SelectValue,
|
|
12
11
|
} from "@/components/ui/select";
|
|
13
|
-
import CountrySelect from "../../components/CountrySelect";
|
|
14
|
-
import { statesFor } from "../../lib/geo-data";
|
|
15
12
|
import { CURRENCIES, CURRENCY_POSITIONS } from "../../lib/constants";
|
|
16
13
|
import useGroupForm from "./useGroupForm";
|
|
17
14
|
|
|
18
15
|
const DEFAULTS = {
|
|
19
|
-
store_name: "",
|
|
20
|
-
store_url: "",
|
|
21
16
|
order_received_path: "/order-received",
|
|
22
|
-
address: { address_1: "", address_2: "", city: "", state: "", postcode: "", country: "US" },
|
|
23
|
-
enable_taxes: true,
|
|
24
|
-
enable_coupons: true,
|
|
25
|
-
calc_discounts_sequentially: false,
|
|
26
17
|
currency: "USD",
|
|
27
18
|
currency_position: "left",
|
|
28
19
|
thousand_sep: ",",
|
|
@@ -32,41 +23,19 @@ const DEFAULTS = {
|
|
|
32
23
|
|
|
33
24
|
export default function GeneralSettings() {
|
|
34
25
|
const { form, setField, SaveButton } = useGroupForm("general", DEFAULTS);
|
|
35
|
-
const address = form.address || DEFAULTS.address;
|
|
36
|
-
const states = statesFor(address.country);
|
|
37
|
-
|
|
38
|
-
const setAddress = (key, value) => setField("address", { ...address, [key]: value });
|
|
39
26
|
|
|
40
27
|
return (
|
|
41
28
|
<div className="max-w-3xl space-y-4">
|
|
42
29
|
<Card>
|
|
43
30
|
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
44
31
|
<div className="space-y-1.5">
|
|
45
|
-
<CardTitle>
|
|
46
|
-
<CardDescription>
|
|
32
|
+
<CardTitle>Checkout</CardTitle>
|
|
33
|
+
<CardDescription>Where customers land once they come back from paying.</CardDescription>
|
|
47
34
|
</div>
|
|
48
35
|
<SaveButton />
|
|
49
36
|
</CardHeader>
|
|
50
|
-
<CardContent
|
|
51
|
-
<div className="space-y-1.5
|
|
52
|
-
<Label>Store name</Label>
|
|
53
|
-
<Input value={form.store_name || ""} onChange={(e) => setField("store_name", e.target.value)} />
|
|
54
|
-
</div>
|
|
55
|
-
<div className="space-y-1.5 sm:col-span-2">
|
|
56
|
-
<Label>Storefront URL</Label>
|
|
57
|
-
<Input
|
|
58
|
-
type="url"
|
|
59
|
-
inputMode="url"
|
|
60
|
-
placeholder="https://shop.example.com"
|
|
61
|
-
value={form.store_url || ""}
|
|
62
|
-
onChange={(e) => setField("store_url", e.target.value)}
|
|
63
|
-
/>
|
|
64
|
-
<p className="text-xs text-muted-foreground">
|
|
65
|
-
Where customers shop. Used to build the page a customer returns to after paying — leave it
|
|
66
|
-
empty and the app's own address is used.
|
|
67
|
-
</p>
|
|
68
|
-
</div>
|
|
69
|
-
<div className="space-y-1.5 sm:col-span-2">
|
|
37
|
+
<CardContent>
|
|
38
|
+
<div className="space-y-1.5">
|
|
70
39
|
<Label>Payment return path</Label>
|
|
71
40
|
<Input
|
|
72
41
|
placeholder="/order-received"
|
|
@@ -74,89 +43,10 @@ export default function GeneralSettings() {
|
|
|
74
43
|
onChange={(e) => setField("order_received_path", e.target.value)}
|
|
75
44
|
/>
|
|
76
45
|
<p className="text-xs text-muted-foreground">
|
|
77
|
-
The storefront route customers come back to after paying
|
|
78
|
-
a wrong path sends paying customers to a 404.
|
|
46
|
+
The storefront route customers come back to after paying, on your storefront's own
|
|
47
|
+
address. It must match the page you built — a wrong path sends paying customers to a 404.
|
|
79
48
|
</p>
|
|
80
49
|
</div>
|
|
81
|
-
<div className="space-y-1.5">
|
|
82
|
-
<Label>Address line 1</Label>
|
|
83
|
-
<Input value={address.address_1 || ""} onChange={(e) => setAddress("address_1", e.target.value)} />
|
|
84
|
-
</div>
|
|
85
|
-
<div className="space-y-1.5">
|
|
86
|
-
<Label>Address line 2</Label>
|
|
87
|
-
<Input value={address.address_2 || ""} onChange={(e) => setAddress("address_2", e.target.value)} />
|
|
88
|
-
</div>
|
|
89
|
-
<div className="space-y-1.5">
|
|
90
|
-
<Label>City</Label>
|
|
91
|
-
<Input value={address.city || ""} onChange={(e) => setAddress("city", e.target.value)} />
|
|
92
|
-
</div>
|
|
93
|
-
<div className="space-y-1.5">
|
|
94
|
-
<Label>Postcode / ZIP</Label>
|
|
95
|
-
<Input value={address.postcode || ""} onChange={(e) => setAddress("postcode", e.target.value)} />
|
|
96
|
-
</div>
|
|
97
|
-
<div className="space-y-1.5">
|
|
98
|
-
<Label>Country</Label>
|
|
99
|
-
<CountrySelect
|
|
100
|
-
value={address.country}
|
|
101
|
-
onChange={(code) => setField("address", { ...address, country: code, state: "" })}
|
|
102
|
-
/>
|
|
103
|
-
</div>
|
|
104
|
-
<div className="space-y-1.5">
|
|
105
|
-
<Label>State / Province</Label>
|
|
106
|
-
{states ? (
|
|
107
|
-
<Select value={address.state || ""} onValueChange={(v) => setAddress("state", v)}>
|
|
108
|
-
<SelectTrigger>
|
|
109
|
-
<SelectValue placeholder="Select state…" />
|
|
110
|
-
</SelectTrigger>
|
|
111
|
-
<SelectContent>
|
|
112
|
-
{states.map((st) => (
|
|
113
|
-
<SelectItem key={st.code} value={st.code}>
|
|
114
|
-
{st.name}
|
|
115
|
-
</SelectItem>
|
|
116
|
-
))}
|
|
117
|
-
</SelectContent>
|
|
118
|
-
</Select>
|
|
119
|
-
) : (
|
|
120
|
-
<Input value={address.state || ""} onChange={(e) => setAddress("state", e.target.value)} />
|
|
121
|
-
)}
|
|
122
|
-
</div>
|
|
123
|
-
</CardContent>
|
|
124
|
-
</Card>
|
|
125
|
-
|
|
126
|
-
<Card>
|
|
127
|
-
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
128
|
-
<div className="space-y-1.5">
|
|
129
|
-
<CardTitle>Store options</CardTitle>
|
|
130
|
-
<CardDescription>
|
|
131
|
-
Where you ship is controlled by your shipping zones — see Settings → Shipping.
|
|
132
|
-
</CardDescription>
|
|
133
|
-
</div>
|
|
134
|
-
<SaveButton />
|
|
135
|
-
</CardHeader>
|
|
136
|
-
<CardContent className="space-y-4">
|
|
137
|
-
<div className="space-y-2">
|
|
138
|
-
<label className="flex items-center gap-2 text-sm">
|
|
139
|
-
<Checkbox
|
|
140
|
-
checked={!!form.enable_taxes}
|
|
141
|
-
onCheckedChange={(v) => setField("enable_taxes", !!v)}
|
|
142
|
-
/>
|
|
143
|
-
Enable tax rates and calculations
|
|
144
|
-
</label>
|
|
145
|
-
<label className="flex items-center gap-2 text-sm">
|
|
146
|
-
<Checkbox
|
|
147
|
-
checked={!!form.enable_coupons}
|
|
148
|
-
onCheckedChange={(v) => setField("enable_coupons", !!v)}
|
|
149
|
-
/>
|
|
150
|
-
Enable the use of coupon codes
|
|
151
|
-
</label>
|
|
152
|
-
<label className="flex items-center gap-2 text-sm">
|
|
153
|
-
<Checkbox
|
|
154
|
-
checked={!!form.calc_discounts_sequentially}
|
|
155
|
-
onCheckedChange={(v) => setField("calc_discounts_sequentially", !!v)}
|
|
156
|
-
/>
|
|
157
|
-
Calculate coupon discounts sequentially
|
|
158
|
-
</label>
|
|
159
|
-
</div>
|
|
160
50
|
</CardContent>
|
|
161
51
|
</Card>
|
|
162
52
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import { Card, CardContent } from "@/components/ui/card";
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { Input } from "@/components/ui/input";
|
|
5
5
|
import { Label } from "@/components/ui/label";
|
|
@@ -88,25 +88,92 @@ export default function PaymentsSettings() {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
return (
|
|
91
|
-
<div className="max-w-3xl
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<div className="
|
|
106
|
-
<div className="
|
|
107
|
-
|
|
91
|
+
<div className="max-w-3xl">
|
|
92
|
+
<Card>
|
|
93
|
+
<CardHeader className="pb-3">
|
|
94
|
+
<CardTitle>Checkout options</CardTitle>
|
|
95
|
+
<CardDescription>
|
|
96
|
+
Which payment options customers can pick at checkout, in this order, and the wording they
|
|
97
|
+
see. This is the storefront's menu only — switching an option on or off, or renaming it,
|
|
98
|
+
does not connect, disconnect or configure any payment provider.
|
|
99
|
+
</CardDescription>
|
|
100
|
+
</CardHeader>
|
|
101
|
+
<CardContent className="divide-y border-t p-0">
|
|
102
|
+
{rows.map((g, i) => {
|
|
103
|
+
const isOnline = g.slug === provider.gatewaySlug;
|
|
104
|
+
return (
|
|
105
|
+
<div key={g.id} className="space-y-3 px-6 py-4">
|
|
106
|
+
<div className="flex items-start gap-4">
|
|
107
|
+
<div className="flex flex-col pt-0.5">
|
|
108
|
+
<Button
|
|
109
|
+
variant="ghost"
|
|
110
|
+
size="icon"
|
|
111
|
+
className="h-6 w-6"
|
|
112
|
+
disabled={i === 0 || busy}
|
|
113
|
+
onClick={() => move(i, -1)}
|
|
114
|
+
>
|
|
115
|
+
<ArrowUp className="h-3.5 w-3.5" />
|
|
116
|
+
</Button>
|
|
117
|
+
<Button
|
|
118
|
+
variant="ghost"
|
|
119
|
+
size="icon"
|
|
120
|
+
className="h-6 w-6"
|
|
121
|
+
disabled={i === rows.length - 1 || busy}
|
|
122
|
+
onClick={() => move(i, 1)}
|
|
123
|
+
>
|
|
124
|
+
<ArrowDown className="h-3.5 w-3.5" />
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div className="min-w-0 flex-1">
|
|
129
|
+
<div className="font-medium">{g.method_title || g.title || g.slug}</div>
|
|
130
|
+
<div className="text-sm text-muted-foreground">
|
|
131
|
+
{g.method_description || g.description}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="flex shrink-0 items-center gap-2 pt-0.5">
|
|
136
|
+
<Switch
|
|
137
|
+
checked={!!g.enabled}
|
|
138
|
+
disabled={busy}
|
|
139
|
+
title={
|
|
140
|
+
isOnline && !provider.connected
|
|
141
|
+
? "Stays hidden from customers until a payment provider is connected"
|
|
142
|
+
: undefined
|
|
143
|
+
}
|
|
144
|
+
onCheckedChange={(v) => toggle(g, v)}
|
|
145
|
+
/>
|
|
146
|
+
{/* Every option's customer-facing wording is editable, the card
|
|
147
|
+
one included — and a button on every row keeps the toggles
|
|
148
|
+
in one column. */}
|
|
149
|
+
<Button
|
|
150
|
+
variant="outline"
|
|
151
|
+
size="sm"
|
|
152
|
+
onClick={() =>
|
|
153
|
+
setDialog({
|
|
154
|
+
gateway: {
|
|
155
|
+
...g,
|
|
156
|
+
settings: {
|
|
157
|
+
...(g.settings || {}),
|
|
158
|
+
account_details:
|
|
159
|
+
g.slug === "offline"
|
|
160
|
+
? g.settings?.account_details || []
|
|
161
|
+
: g.settings?.account_details,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
>
|
|
167
|
+
<Settings2 className="mr-2 h-4 w-4" />
|
|
168
|
+
Labels
|
|
169
|
+
</Button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Full width, below the row: inside it the tall notice drags the
|
|
174
|
+
arrows and the switch down to the vertical middle. */}
|
|
108
175
|
{isOnline && !provider.loading && (
|
|
109
|
-
<Alert
|
|
176
|
+
<Alert>
|
|
110
177
|
<Info className="h-4 w-4" />
|
|
111
178
|
{provider.connected ? (
|
|
112
179
|
<>
|
|
@@ -121,11 +188,11 @@ export default function PaymentsSettings() {
|
|
|
121
188
|
<>
|
|
122
189
|
<AlertTitle>No payment provider connected</AlertTitle>
|
|
123
190
|
<AlertDescription>
|
|
124
|
-
Card payments are ready to go but need a provider:
|
|
125
|
-
{provider.providerLabel || "payment"}
|
|
126
|
-
this option starts appearing at checkout — no code to
|
|
127
|
-
hidden from customers, so nobody can pick it and
|
|
128
|
-
provider? See{" "}
|
|
191
|
+
Card payments are ready to go but need a provider: set{" "}
|
|
192
|
+
{provider.providerLabel || "a payment provider"} up in this app's
|
|
193
|
+
integrations, and this option starts appearing at checkout — no code to
|
|
194
|
+
write. Until then it stays hidden from customers, so nobody can pick it and
|
|
195
|
+
pay nothing. Prefer a different provider? See{" "}
|
|
129
196
|
<code className="text-xs">skills/commerce/references/online-payments.md</code>.
|
|
130
197
|
</AlertDescription>
|
|
131
198
|
</>
|
|
@@ -133,42 +200,10 @@ export default function PaymentsSettings() {
|
|
|
133
200
|
</Alert>
|
|
134
201
|
)}
|
|
135
202
|
</div>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
title={
|
|
141
|
-
isOnline && !provider.connected
|
|
142
|
-
? "Stays hidden from customers until a payment provider is connected"
|
|
143
|
-
: undefined
|
|
144
|
-
}
|
|
145
|
-
onCheckedChange={(v) => toggle(g, v)}
|
|
146
|
-
/>
|
|
147
|
-
{!isOnline && (
|
|
148
|
-
<Button
|
|
149
|
-
variant="outline"
|
|
150
|
-
size="sm"
|
|
151
|
-
onClick={() =>
|
|
152
|
-
setDialog({
|
|
153
|
-
gateway: {
|
|
154
|
-
...g,
|
|
155
|
-
settings: {
|
|
156
|
-
...(g.settings || {}),
|
|
157
|
-
account_details: g.slug === "bacs" ? (g.settings?.account_details || []) : g.settings?.account_details,
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
})
|
|
161
|
-
}
|
|
162
|
-
>
|
|
163
|
-
<Settings2 className="mr-2 h-4 w-4" />
|
|
164
|
-
Manage
|
|
165
|
-
</Button>
|
|
166
|
-
)}
|
|
167
|
-
</div>
|
|
168
|
-
</CardContent>
|
|
169
|
-
</Card>
|
|
170
|
-
);
|
|
171
|
-
})}
|
|
203
|
+
);
|
|
204
|
+
})}
|
|
205
|
+
</CardContent>
|
|
206
|
+
</Card>
|
|
172
207
|
|
|
173
208
|
{dialog && (
|
|
174
209
|
<Dialog open onOpenChange={(o) => !o && setDialog(null)}>
|
|
@@ -194,7 +229,7 @@ export default function PaymentsSettings() {
|
|
|
194
229
|
/>
|
|
195
230
|
</div>
|
|
196
231
|
|
|
197
|
-
{dialog.gateway.slug === "
|
|
232
|
+
{dialog.gateway.slug === "offline" && (
|
|
198
233
|
<BankAccountsEditor
|
|
199
234
|
accounts={dialog.gateway.settings?.account_details || []}
|
|
200
235
|
onChange={(account_details) =>
|
|
@@ -225,7 +260,11 @@ function BankAccountsEditor({ accounts, onChange }) {
|
|
|
225
260
|
return (
|
|
226
261
|
<div className="space-y-3">
|
|
227
262
|
<Label>Bank account details</Label>
|
|
228
|
-
{accounts.length === 0 &&
|
|
263
|
+
{accounts.length === 0 && (
|
|
264
|
+
<p className="text-xs text-muted-foreground">
|
|
265
|
+
None yet — fine for cash on delivery; add one for bank transfer.
|
|
266
|
+
</p>
|
|
267
|
+
)}
|
|
229
268
|
{accounts.map((a, i) => (
|
|
230
269
|
<div key={i} className="space-y-2 rounded-md border p-3">
|
|
231
270
|
<div className="flex items-center justify-between">
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
|
3
|
-
import { useSettings } from "../../context/SettingsContext";
|
|
4
3
|
import { useAdminHref } from "../../context/BasePathContext";
|
|
5
4
|
import PageHeader from "../../components/PageHeader";
|
|
6
5
|
|
|
@@ -12,11 +11,11 @@ const TABS = [
|
|
|
12
11
|
{ seg: "shipping", label: "Shipping" },
|
|
13
12
|
{ seg: "payments", label: "Payments" },
|
|
14
13
|
{ seg: "emails", label: "Emails" },
|
|
14
|
+
{ seg: "webhooks", label: "Webhooks" },
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
/** Layout route for /settings/*: horizontal group tabs + nested page via <Outlet/>. */
|
|
18
18
|
export default function SettingsLayout() {
|
|
19
|
-
const { get } = useSettings();
|
|
20
19
|
const href = useAdminHref();
|
|
21
20
|
const navigate = useNavigate();
|
|
22
21
|
const location = useLocation();
|
|
@@ -25,14 +24,11 @@ export default function SettingsLayout() {
|
|
|
25
24
|
const afterSettings = location.pathname.split("/settings/")[1] || "general";
|
|
26
25
|
const current = afterSettings.split("/")[0] || "general";
|
|
27
26
|
|
|
28
|
-
const enableTaxes = get("general", "enable_taxes", true);
|
|
29
|
-
const tabs = TABS.filter((t) => t.seg !== "tax" || enableTaxes);
|
|
30
|
-
|
|
31
27
|
return (
|
|
32
28
|
<div className="space-y-4">
|
|
33
29
|
<PageHeader title="Settings" description="Configure how your store works." />
|
|
34
30
|
<div className="flex flex-wrap gap-1 border-b">
|
|
35
|
-
{
|
|
31
|
+
{TABS.map((t) => (
|
|
36
32
|
<button
|
|
37
33
|
key={t.seg}
|
|
38
34
|
type="button"
|
|
@@ -75,7 +75,6 @@ function TaxOptions({ classes }) {
|
|
|
75
75
|
<SelectContent>
|
|
76
76
|
<SelectItem value="shipping">Customer shipping address</SelectItem>
|
|
77
77
|
<SelectItem value="billing">Customer billing address</SelectItem>
|
|
78
|
-
<SelectItem value="base">Shop base address</SelectItem>
|
|
79
78
|
</SelectContent>
|
|
80
79
|
</Select>
|
|
81
80
|
</div>
|
|
@@ -86,7 +86,7 @@ export default function WebhookEditor() {
|
|
|
86
86
|
if (isNew) {
|
|
87
87
|
const created = await base44.entities["commerce.Webhook"].create(payload);
|
|
88
88
|
toast.success("Webhook created");
|
|
89
|
-
navigate(href(`webhooks/${created.id}`));
|
|
89
|
+
navigate(href(`settings/webhooks/${created.id}`));
|
|
90
90
|
} else {
|
|
91
91
|
await base44.entities["commerce.Webhook"].update(id, payload);
|
|
92
92
|
toast.success("Webhook saved");
|
|
@@ -103,7 +103,7 @@ export default function WebhookEditor() {
|
|
|
103
103
|
try {
|
|
104
104
|
await base44.entities["commerce.Webhook"].delete(id);
|
|
105
105
|
toast.success("Webhook deleted");
|
|
106
|
-
navigate(href("webhooks"));
|
|
106
|
+
navigate(href("settings/webhooks"));
|
|
107
107
|
} catch (err) {
|
|
108
108
|
toast.error(err.message || "Failed to delete");
|
|
109
109
|
setDeleting(false);
|
|
@@ -135,7 +135,7 @@ export default function WebhookEditor() {
|
|
|
135
135
|
<div className="max-w-3xl space-y-4">
|
|
136
136
|
<PageHeader
|
|
137
137
|
title={isNew ? "Add webhook" : "Edit webhook"}
|
|
138
|
-
backHref={href("webhooks")}
|
|
138
|
+
backHref={href("settings/webhooks")}
|
|
139
139
|
actions={
|
|
140
140
|
!isNew && (
|
|
141
141
|
<>
|
|
@@ -35,7 +35,7 @@ export default function Webhooks() {
|
|
|
35
35
|
title="Webhooks"
|
|
36
36
|
description="Notify external services when store events occur."
|
|
37
37
|
actions={
|
|
38
|
-
<Button onClick={() => navigate(href("webhooks/new"))}>
|
|
38
|
+
<Button onClick={() => navigate(href("settings/webhooks/new"))}>
|
|
39
39
|
<Plus className="mr-2 h-4 w-4" />
|
|
40
40
|
Add webhook
|
|
41
41
|
</Button>
|
|
@@ -45,7 +45,7 @@ export default function Webhooks() {
|
|
|
45
45
|
columns={columns}
|
|
46
46
|
rows={hooks.data || []}
|
|
47
47
|
loading={hooks.loading}
|
|
48
|
-
onRowClick={(w) => navigate(href(`webhooks/${w.id}`))}
|
|
48
|
+
onRowClick={(w) => navigate(href(`settings/webhooks/${w.id}`))}
|
|
49
49
|
empty={{ icon: WebhookIcon, title: "No webhooks", description: "Create a webhook to POST event payloads to an external URL." }}
|
|
50
50
|
/>
|
|
51
51
|
</div>
|
|
@@ -11,9 +11,6 @@ import OrderEditor from "./pages/orders/OrderEditor";
|
|
|
11
11
|
import ProductsList from "./pages/products/ProductsList";
|
|
12
12
|
import ProductEditor from "./pages/products/ProductEditor";
|
|
13
13
|
import Categories from "./pages/products/Categories";
|
|
14
|
-
import Tags from "./pages/products/Tags";
|
|
15
|
-
import Attributes from "./pages/products/Attributes";
|
|
16
|
-
import AttributeTerms from "./pages/products/AttributeTerms";
|
|
17
14
|
import Reviews from "./pages/products/Reviews";
|
|
18
15
|
import CouponsList from "./pages/coupons/CouponsList";
|
|
19
16
|
import CouponEditor from "./pages/coupons/CouponEditor";
|
|
@@ -44,9 +41,6 @@ export const ADMIN_ROUTES = [
|
|
|
44
41
|
{ path: "products", label: "Products" },
|
|
45
42
|
{ path: "products/new", label: "Add product" },
|
|
46
43
|
{ path: "products/categories", label: "Categories" },
|
|
47
|
-
{ path: "products/tags", label: "Tags" },
|
|
48
|
-
{ path: "products/attributes", label: "Attributes" },
|
|
49
|
-
{ path: "products/attributes/:id/terms", label: "Terms" },
|
|
50
44
|
{ path: "products/reviews", label: "Reviews" },
|
|
51
45
|
{ path: "products/:id", label: "Edit product" },
|
|
52
46
|
{ path: "coupons", label: "Coupons" },
|
|
@@ -65,9 +59,9 @@ export const ADMIN_ROUTES = [
|
|
|
65
59
|
{ path: "settings/shipping/zones/:zoneId", label: "Shipping zone" },
|
|
66
60
|
{ path: "settings/payments", label: "Payments" },
|
|
67
61
|
{ path: "settings/emails", label: "Emails" },
|
|
68
|
-
{ path: "webhooks", label: "Webhooks" },
|
|
69
|
-
{ path: "webhooks/new", label: "Add webhook" },
|
|
70
|
-
{ path: "webhooks/:id", label: "Webhook" },
|
|
62
|
+
{ path: "settings/webhooks", label: "Webhooks" },
|
|
63
|
+
{ path: "settings/webhooks/new", label: "Add webhook" },
|
|
64
|
+
{ path: "settings/webhooks/:id", label: "Webhook" },
|
|
71
65
|
];
|
|
72
66
|
|
|
73
67
|
function NotFound() {
|
|
@@ -89,8 +83,9 @@ function NotFound() {
|
|
|
89
83
|
|
|
90
84
|
/**
|
|
91
85
|
* Catch-all. A URL that still contains the *mount pattern* rather than a real
|
|
92
|
-
* path — `/admin/*` (the `<Route path="/admin/*">` pattern pasted
|
|
93
|
-
* address bar or copied into a nav link), or a leftover
|
|
86
|
+
* path — `/store-admin/*` (the `<Route path="/store-admin/*">` pattern pasted
|
|
87
|
+
* into the address bar or copied into a nav link), or a leftover
|
|
88
|
+
* `/store-admin/:id` — is a
|
|
94
89
|
* wiring artifact, not a page the admin asked for: land on the dashboard
|
|
95
90
|
* instead of showing a dead end. Anything else is a genuine 404.
|
|
96
91
|
*/
|
|
@@ -100,7 +95,7 @@ function UnmatchedRoute() {
|
|
|
100
95
|
return isMountPatternPath(params["*"]) ? <Navigate to={href()} replace /> : <NotFound />;
|
|
101
96
|
}
|
|
102
97
|
|
|
103
|
-
/** All admin routes. Rendered inside AdminLayout; mount AdminApp at `/admin/*`. */
|
|
98
|
+
/** All admin routes. Rendered inside AdminLayout; mount AdminApp at `/store-admin/*`. */
|
|
104
99
|
export default function AdminRoutes() {
|
|
105
100
|
return (
|
|
106
101
|
<Routes>
|
|
@@ -113,9 +108,6 @@ export default function AdminRoutes() {
|
|
|
113
108
|
<Route path="products" element={<ProductsList />} />
|
|
114
109
|
<Route path="products/new" element={<ProductEditor />} />
|
|
115
110
|
<Route path="products/categories" element={<Categories />} />
|
|
116
|
-
<Route path="products/tags" element={<Tags />} />
|
|
117
|
-
<Route path="products/attributes" element={<Attributes />} />
|
|
118
|
-
<Route path="products/attributes/:id/terms" element={<AttributeTerms />} />
|
|
119
111
|
<Route path="products/reviews" element={<Reviews />} />
|
|
120
112
|
<Route path="products/:id" element={<ProductEditor />} />
|
|
121
113
|
|
|
@@ -139,12 +131,11 @@ export default function AdminRoutes() {
|
|
|
139
131
|
<Route path="shipping/zones/:zoneId" element={<ShippingZoneEditor />} />
|
|
140
132
|
<Route path="payments" element={<PaymentsSettings />} />
|
|
141
133
|
<Route path="emails" element={<EmailsSettings />} />
|
|
134
|
+
<Route path="webhooks" element={<Webhooks />} />
|
|
135
|
+
<Route path="webhooks/new" element={<WebhookEditor />} />
|
|
136
|
+
<Route path="webhooks/:id" element={<WebhookEditor />} />
|
|
142
137
|
</Route>
|
|
143
138
|
|
|
144
|
-
<Route path="webhooks" element={<Webhooks />} />
|
|
145
|
-
<Route path="webhooks/new" element={<WebhookEditor />} />
|
|
146
|
-
<Route path="webhooks/:id" element={<WebhookEditor />} />
|
|
147
|
-
|
|
148
139
|
<Route path="*" element={<UnmatchedRoute />} />
|
|
149
140
|
</Routes>
|
|
150
141
|
);
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* import { resolveSelection, defaultSelection, selectOption } from "@/commerce/utils";
|
|
10
10
|
*
|
|
11
11
|
* - `variants.js` — variant selection: map attribute selections (Size, Color)
|
|
12
|
-
* to a `ProductVariation` and back, per-option availability,
|
|
13
|
-
*
|
|
12
|
+
* to a `ProductVariation` and back, per-option availability, variant price
|
|
13
|
+
* ranges. See `skills/commerce/references/storefront-product-page.md`.
|
|
14
14
|
* - `shipping-promos.js` — read the store's real free-shipping configuration so
|
|
15
15
|
* "Free shipping over €150" copy states a configured rule, not an invented
|
|
16
16
|
* number. See `skills/commerce/docs/api-storefront.md`.
|