@deenruv/upsell-plugin 1.0.17-dev.30 → 1.0.17-dev.32
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/plugin-ui/components/UpsellSelect.js +19 -19
- package/dist/plugin-ui/index.js +2 -1
- package/dist/plugin-ui/locales/en/index.d.ts +23 -2
- package/dist/plugin-ui/locales/en/index.js +3 -2
- package/dist/plugin-ui/locales/en/upsell.json +22 -0
- package/dist/plugin-ui/locales/pl/index.d.ts +23 -2
- package/dist/plugin-ui/locales/pl/index.js +3 -2
- package/dist/plugin-ui/locales/pl/upsell.json +22 -0
- package/dist/plugin-ui/translations.spec.d.ts +1 -0
- package/dist/plugin-ui/translations.spec.js +45 -0
- package/package.json +5 -5
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { buttonVariants, Card, CardContent, CardHeader, DialogProductPicker, Routes, ScrollArea, useDetailView, useLazyQuery, useMutation, } from "@deenruv/react-ui-devkit";
|
|
2
|
+
import { buttonVariants, Card, CardContent, CardHeader, DialogProductPicker, Routes, ScrollArea, useDetailView, useLazyQuery, useMutation, useTranslation, } from "@deenruv/react-ui-devkit";
|
|
3
3
|
import React, { useEffect, useState } from "react";
|
|
4
|
-
import { useNavigate } from "react-router";
|
|
5
4
|
import { QUERIES } from "../graphql/queries";
|
|
6
5
|
import { MUTATIONS } from "../graphql/mutations";
|
|
7
6
|
import { toast } from "sonner";
|
|
8
7
|
import { Loader2, PlusCircle, XIcon } from "lucide-react";
|
|
8
|
+
import { translationNS } from "../translation-ns";
|
|
9
9
|
export const UpsellSelect = () => {
|
|
10
|
+
const { t } = useTranslation(translationNS);
|
|
10
11
|
const { id } = useDetailView("products-detail-view");
|
|
11
|
-
const navigate = useNavigate();
|
|
12
12
|
const [run, { data, loading }] = useLazyQuery(QUERIES["GET_UPSELLS"]);
|
|
13
|
-
const [createUpsell
|
|
14
|
-
const [deleteUpsell
|
|
13
|
+
const [createUpsell] = useMutation(MUTATIONS["CREATE_UPSELL"]);
|
|
14
|
+
const [deleteUpsell] = useMutation(MUTATIONS["DELETE_UPSELL"]);
|
|
15
15
|
const [deletingIds, setDeletingIds] = useState([]);
|
|
16
16
|
const onSingleDelete = async (upsellProductID) => {
|
|
17
17
|
if (!id) {
|
|
18
|
-
toast.error("
|
|
18
|
+
toast.error(t("toast.noProduct"));
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
setDeletingIds((prev) => [...prev, upsellProductID]);
|
|
@@ -24,10 +24,10 @@ export const UpsellSelect = () => {
|
|
|
24
24
|
input: [{ baseProductID: id, upsellProductID }],
|
|
25
25
|
});
|
|
26
26
|
run({ productID: id });
|
|
27
|
-
toast.success("
|
|
27
|
+
toast.success(t("toast.removed"));
|
|
28
28
|
}
|
|
29
29
|
catch {
|
|
30
|
-
toast.error("
|
|
30
|
+
toast.error(t("toast.removeFailed"));
|
|
31
31
|
}
|
|
32
32
|
finally {
|
|
33
33
|
setDeletingIds((prev) => prev.filter((id) => id !== upsellProductID));
|
|
@@ -40,14 +40,14 @@ export const UpsellSelect = () => {
|
|
|
40
40
|
return (React.createElement(Card, { className: "border border-border shadow-sm" },
|
|
41
41
|
React.createElement(CardHeader, { className: "border-b border-border bg-muted/30" },
|
|
42
42
|
React.createElement("div", { className: "flex items-center justify-between" },
|
|
43
|
-
React.createElement("h3", { className: "text-lg font-semibold" }, "
|
|
43
|
+
React.createElement("h3", { className: "text-lg font-semibold" }, t("title")),
|
|
44
44
|
React.createElement(DialogProductPicker, { initialValue: data?.upsellProducts.map((p) => p.id) || [], mode: "product", multiple: true, onSubmit: async (result) => {
|
|
45
45
|
if (!id) {
|
|
46
|
-
toast.error("
|
|
46
|
+
toast.error(t("toast.noProduct"));
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
if (!result) {
|
|
50
|
-
toast.info("
|
|
50
|
+
toast.info(t("toast.selectionCanceled"));
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
const selectedIds = result.map((p) => p.productId);
|
|
@@ -72,18 +72,18 @@ export const UpsellSelect = () => {
|
|
|
72
72
|
})),
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
-
toast.success("
|
|
75
|
+
toast.success(t("toast.updated"));
|
|
76
76
|
}
|
|
77
77
|
catch {
|
|
78
|
-
toast.error("
|
|
78
|
+
toast.error(t("toast.updateFailed"));
|
|
79
79
|
}
|
|
80
80
|
run({ productID: id });
|
|
81
81
|
} }))),
|
|
82
|
-
React.createElement(CardContent, { className: "p-6" }, loading ? (React.createElement("div", { className: "flex h-[400px] items-center justify-center" },
|
|
82
|
+
React.createElement(CardContent, { className: "p-6" }, loading ? (React.createElement("div", { className: "flex h-[400px] items-center justify-center", role: "status", "aria-label": t("loading") },
|
|
83
83
|
React.createElement(Loader2, { className: "h-8 w-8 animate-spin text-muted-foreground" }))) : (React.createElement(ScrollArea, { className: "h-[400px]" }, data?.upsellProducts?.length ? (React.createElement("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4" }, data.upsellProducts.map((product) => (React.createElement("div", { key: product.id, className: "group relative flex flex-col overflow-hidden rounded-lg border border-border bg-card transition-all hover:shadow-md" },
|
|
84
84
|
React.createElement("div", { className: "relative aspect-square overflow-hidden bg-muted/20" },
|
|
85
|
-
product.featuredAsset?.preview ? (React.createElement("img", { src: product.featuredAsset.preview || "/placeholder.svg", alt: product.name, className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" })) : (React.createElement("div", { className: "flex h-full w-full items-center justify-center bg-muted/30 text-muted-foreground" }, "
|
|
86
|
-
React.createElement("button", { className: "absolute right-2 top-2 rounded-full bg-background/80 p-1.5 text-destructive opacity-0 shadow-sm transition-opacity hover:bg-destructive hover:text-destructive-foreground focus:opacity-100 group-hover:opacity-100", onClick: () => onSingleDelete(product.id), disabled: deletingIds.includes(product.id), "aria-label": "
|
|
85
|
+
product.featuredAsset?.preview ? (React.createElement("img", { src: product.featuredAsset.preview || "/placeholder.svg", alt: product.name, className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" })) : (React.createElement("div", { className: "flex h-full w-full items-center justify-center bg-muted/30 text-muted-foreground" }, t("noImage"))),
|
|
86
|
+
React.createElement("button", { className: "absolute right-2 top-2 rounded-full bg-background/80 p-1.5 text-destructive opacity-0 shadow-sm transition-opacity hover:bg-destructive hover:text-destructive-foreground focus:opacity-100 group-hover:opacity-100", onClick: () => onSingleDelete(product.id), disabled: deletingIds.includes(product.id), "aria-label": t("removeAria") }, deletingIds.includes(product.id) ? (React.createElement(Loader2, { className: "h-4 w-4 animate-spin" })) : (React.createElement(XIcon, { className: "h-4 w-4" })))),
|
|
87
87
|
React.createElement("div", { className: "flex flex-1 flex-col p-3" },
|
|
88
88
|
React.createElement("h4", { className: "line-clamp-2 font-medium leading-tight text-sm mb-3", title: product.name }, product.name),
|
|
89
89
|
React.createElement("div", { className: "mt-auto" },
|
|
@@ -91,9 +91,9 @@ export const UpsellSelect = () => {
|
|
|
91
91
|
variant: "secondary",
|
|
92
92
|
size: "sm",
|
|
93
93
|
className: "w-full text-xs",
|
|
94
|
-
}), rel: "noreferrer" }, "
|
|
94
|
+
}), rel: "noreferrer" }, t("viewProduct"))))))))) : (React.createElement("div", { className: "flex h-[300px] flex-col items-center justify-center rounded-lg border border-dashed border-muted-foreground/20 p-8 text-center" },
|
|
95
95
|
React.createElement("div", { className: "rounded-full bg-muted/30 p-3" },
|
|
96
96
|
React.createElement(PlusCircle, { className: "h-6 w-6 text-muted-foreground" })),
|
|
97
|
-
React.createElement("h3", { className: "mt-4 text-lg font-medium" }, "
|
|
98
|
-
React.createElement("p", { className: "mt-2 text-sm text-muted-foreground" }, "
|
|
97
|
+
React.createElement("h3", { className: "mt-4 text-lg font-medium" }, t("empty.title")),
|
|
98
|
+
React.createElement("p", { className: "mt-2 text-sm text-muted-foreground" }, t("empty.description")))))))));
|
|
99
99
|
};
|
package/dist/plugin-ui/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createDeenruvUIPlugin, DEENRUV_UI_VERSION } from "@deenruv/react-ui-devkit";
|
|
1
|
+
import { createDeenruvUIPlugin, DEENRUV_UI_VERSION, } from "@deenruv/react-ui-devkit";
|
|
2
2
|
import pl from "./locales/pl";
|
|
3
3
|
import en from "./locales/en";
|
|
4
4
|
import { translationNS } from "./translation-ns";
|
|
@@ -16,6 +16,7 @@ export const UpsellUIPlugin = createDeenruvUIPlugin({
|
|
|
16
16
|
sidebarReplacement: React.createElement(React.Fragment, null),
|
|
17
17
|
name: "Extras",
|
|
18
18
|
label: "Extras",
|
|
19
|
+
labelId: "tab.extras",
|
|
19
20
|
},
|
|
20
21
|
],
|
|
21
22
|
});
|
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
declare const _default: {
|
|
1
|
+
declare const _default: ({
|
|
2
2
|
createReceipt: string;
|
|
3
3
|
createProformaInvoice: string;
|
|
4
|
-
}
|
|
4
|
+
} | {
|
|
5
|
+
tab: {
|
|
6
|
+
extras: string;
|
|
7
|
+
};
|
|
8
|
+
title: string;
|
|
9
|
+
loading: string;
|
|
10
|
+
noImage: string;
|
|
11
|
+
removeAria: string;
|
|
12
|
+
viewProduct: string;
|
|
13
|
+
empty: {
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
toast: {
|
|
18
|
+
noProduct: string;
|
|
19
|
+
selectionCanceled: string;
|
|
20
|
+
removed: string;
|
|
21
|
+
removeFailed: string;
|
|
22
|
+
updated: string;
|
|
23
|
+
updateFailed: string;
|
|
24
|
+
};
|
|
25
|
+
})[];
|
|
5
26
|
export default _default;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import wfirma from
|
|
2
|
-
|
|
1
|
+
import wfirma from "./wfirma.json";
|
|
2
|
+
import upsell from "./upsell.json";
|
|
3
|
+
export default [wfirma, upsell];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tab": {
|
|
3
|
+
"extras": "Extras"
|
|
4
|
+
},
|
|
5
|
+
"title": "Upsell products",
|
|
6
|
+
"loading": "Loading upsell products",
|
|
7
|
+
"noImage": "No image",
|
|
8
|
+
"removeAria": "Remove upsell product",
|
|
9
|
+
"viewProduct": "View product",
|
|
10
|
+
"empty": {
|
|
11
|
+
"title": "No upsell products",
|
|
12
|
+
"description": "Add upsell products to increase average order value"
|
|
13
|
+
},
|
|
14
|
+
"toast": {
|
|
15
|
+
"noProduct": "No product selected",
|
|
16
|
+
"selectionCanceled": "Upsell product selection canceled",
|
|
17
|
+
"removed": "Upsell product removed",
|
|
18
|
+
"removeFailed": "Failed to remove upsell product",
|
|
19
|
+
"updated": "Upsell products updated successfully",
|
|
20
|
+
"updateFailed": "Failed to update upsell products"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
declare const _default: {
|
|
1
|
+
declare const _default: ({
|
|
2
2
|
createReceipt: string;
|
|
3
3
|
createProformaInvoice: string;
|
|
4
|
-
}
|
|
4
|
+
} | {
|
|
5
|
+
tab: {
|
|
6
|
+
extras: string;
|
|
7
|
+
};
|
|
8
|
+
title: string;
|
|
9
|
+
loading: string;
|
|
10
|
+
noImage: string;
|
|
11
|
+
removeAria: string;
|
|
12
|
+
viewProduct: string;
|
|
13
|
+
empty: {
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
toast: {
|
|
18
|
+
noProduct: string;
|
|
19
|
+
selectionCanceled: string;
|
|
20
|
+
removed: string;
|
|
21
|
+
removeFailed: string;
|
|
22
|
+
updated: string;
|
|
23
|
+
updateFailed: string;
|
|
24
|
+
};
|
|
25
|
+
})[];
|
|
5
26
|
export default _default;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import wfirma from
|
|
2
|
-
|
|
1
|
+
import wfirma from "./wfirma.json";
|
|
2
|
+
import upsell from "./upsell.json";
|
|
3
|
+
export default [wfirma, upsell];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tab": {
|
|
3
|
+
"extras": "Dodatki"
|
|
4
|
+
},
|
|
5
|
+
"title": "Produkty upsell",
|
|
6
|
+
"loading": "Ładowanie produktów upsell",
|
|
7
|
+
"noImage": "Brak zdjęcia",
|
|
8
|
+
"removeAria": "Usuń produkt upsell",
|
|
9
|
+
"viewProduct": "Zobacz produkt",
|
|
10
|
+
"empty": {
|
|
11
|
+
"title": "Brak produktów upsell",
|
|
12
|
+
"description": "Dodaj produkty upsell, aby zwiększyć średnią wartość zamówienia"
|
|
13
|
+
},
|
|
14
|
+
"toast": {
|
|
15
|
+
"noProduct": "Nie wybrano produktu",
|
|
16
|
+
"selectionCanceled": "Anulowano wybór produktów upsell",
|
|
17
|
+
"removed": "Produkt upsell został usunięty",
|
|
18
|
+
"removeFailed": "Nie udało się usunąć produktu upsell",
|
|
19
|
+
"updated": "Produkty upsell zostały zaktualizowane",
|
|
20
|
+
"updateFailed": "Nie udało się zaktualizować produktów upsell"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath, URL } from "node:url";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
const readJson = (relativePath) => JSON.parse(readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8"));
|
|
5
|
+
const flattenKeys = (tree, prefix = "") => Object.entries(tree).flatMap(([key, value]) => {
|
|
6
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
7
|
+
return typeof value === "string" ? [path] : flattenKeys(value, path);
|
|
8
|
+
});
|
|
9
|
+
const componentSource = readFileSync(fileURLToPath(new URL("./components/UpsellSelect.tsx", import.meta.url)), "utf8");
|
|
10
|
+
const pluginSource = readFileSync(fileURLToPath(new URL("./index.tsx", import.meta.url)), "utf8");
|
|
11
|
+
describe("upsell plugin translations", () => {
|
|
12
|
+
const en = readJson("./locales/en/upsell.json");
|
|
13
|
+
const pl = readJson("./locales/pl/upsell.json");
|
|
14
|
+
it("keeps complete recursive EN/PL key parity", () => {
|
|
15
|
+
expect(flattenKeys(pl).sort()).toEqual(flattenKeys(en).sort());
|
|
16
|
+
});
|
|
17
|
+
it("defines every statically referenced translation key", () => {
|
|
18
|
+
const referencedKeys = Array.from(componentSource.matchAll(/\bt\("([^"]+)"/g), (match) => match[1]);
|
|
19
|
+
const availableKeys = new Set(flattenKeys(en));
|
|
20
|
+
expect(referencedKeys.length).toBeGreaterThan(0);
|
|
21
|
+
expect(referencedKeys.filter((key) => !availableKeys.has(key))).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
it("declares an SSR-safe product-detail tab translation key", () => {
|
|
24
|
+
expect(pluginSource).toContain('labelId: "tab.extras"');
|
|
25
|
+
expect(pluginSource).toContain('label: "Extras"');
|
|
26
|
+
expect(pluginSource).not.toContain("get label()");
|
|
27
|
+
expect(pluginSource).not.toContain("globalThis.window");
|
|
28
|
+
expect(en.tab).toEqual({ extras: "Extras" });
|
|
29
|
+
expect(pl.tab).toEqual({ extras: "Dodatki" });
|
|
30
|
+
});
|
|
31
|
+
it("does not reintroduce audited hard-coded shell copy", () => {
|
|
32
|
+
for (const literal of [
|
|
33
|
+
">Upsell Products<",
|
|
34
|
+
">No image<",
|
|
35
|
+
'aria-label="Remove upsell product"',
|
|
36
|
+
">View Product<",
|
|
37
|
+
">No upsell products<",
|
|
38
|
+
"Add upsell products to increase average order value",
|
|
39
|
+
'toast.error("No product selected")',
|
|
40
|
+
'toast.success("Upsell product removed")',
|
|
41
|
+
]) {
|
|
42
|
+
expect(componentSource).not.toContain(literal);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deenruv/upsell-plugin",
|
|
3
|
-
"version": "1.0.17-dev.
|
|
3
|
+
"version": "1.0.17-dev.32",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"react-i18next": "^14.0.5",
|
|
31
31
|
"sonner": "^1.4.41",
|
|
32
32
|
"recharts": "^2.12.7",
|
|
33
|
-
"@deenruv/
|
|
34
|
-
"@deenruv/
|
|
35
|
-
"@deenruv/
|
|
33
|
+
"@deenruv/common": "^1.0.17-dev.32",
|
|
34
|
+
"@deenruv/react-ui-devkit": "^1.0.17-dev.32",
|
|
35
|
+
"@deenruv/admin-types": "^1.0.17-dev.32"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@graphql-typed-document-node/core": "3.2.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"rimraf": "^5.0.10",
|
|
45
45
|
"tslib": "^2.6.2",
|
|
46
46
|
"typescript": "5.3.3",
|
|
47
|
-
"@deenruv/core": "^1.0.17-dev.
|
|
47
|
+
"@deenruv/core": "^1.0.17-dev.32"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@deenruv/core": "^1.0.0 || ^1.0.17-dev.0"
|