update item nav
This commit is contained in:
parent
09b14bc8ac
commit
3c418952d5
|
|
@ -30,7 +30,8 @@
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|
@ -8391,6 +8392,35 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,6 @@
|
||||||
|
import DataSession from "@/components/home/data-session";
|
||||||
import HomePagination from "@/components/home/home-pagination";
|
import HomePagination from "@/components/home/home-pagination";
|
||||||
import Item from "@/components/home/item";
|
|
||||||
import SKUListSidebar from "@/components/home/sku-list-sidebar";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Empty,
|
|
||||||
EmptyContent,
|
|
||||||
EmptyDescription,
|
|
||||||
EmptyHeader,
|
|
||||||
EmptyMedia,
|
|
||||||
EmptyTitle,
|
|
||||||
} from "@/components/ui/empty";
|
|
||||||
import { Sku } from "@/entities/Sku";
|
|
||||||
import axios from "@/lib/axios";
|
import axios from "@/lib/axios";
|
||||||
import { Box } from "lucide-react";
|
|
||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
|
|
||||||
const getData = async (query: { page: number; search: string }) => {
|
const getData = async (query: { page: number; search: string }) => {
|
||||||
|
|
@ -39,41 +27,7 @@ export default function Home({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-12 min-h-[1000px] px-5 gap-4 py-2 pb-20">
|
<div className="grid grid-cols-12 min-h-[1000px] px-5 gap-4 py-2 pb-20">
|
||||||
<div className="col-span-8 display flex flex-col gap-10">
|
<DataSession data={data} />
|
||||||
{data?.data?.length > 0 &&
|
|
||||||
(data.data || []).map(
|
|
||||||
(
|
|
||||||
item: Pick<
|
|
||||||
Sku,
|
|
||||||
| "id"
|
|
||||||
| "normalized_title"
|
|
||||||
| "normalized_short_description"
|
|
||||||
| "sku"
|
|
||||||
| "normalized_html"
|
|
||||||
| "status"
|
|
||||||
>,
|
|
||||||
index: number
|
|
||||||
) => {
|
|
||||||
return <Item key={item?.sku || index} data={item} />;
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data?.data?.length <= 0 && (
|
|
||||||
<Empty>
|
|
||||||
<EmptyHeader>
|
|
||||||
<EmptyMedia variant="icon">
|
|
||||||
<Box />
|
|
||||||
</EmptyMedia>
|
|
||||||
<EmptyTitle>No Products</EmptyTitle>
|
|
||||||
</EmptyHeader>
|
|
||||||
</Empty>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-span-4 -my-2 border-l">
|
|
||||||
<div className="sticky z-50 top-0">
|
|
||||||
<SKUListSidebar data={data?.data} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<HomePagination page={page} totalPages={data.pagination.totalPages} />
|
<HomePagination page={page} totalPages={data.pagination.totalPages} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Box } from "lucide-react";
|
||||||
|
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
|
||||||
|
import Item from "./item";
|
||||||
|
import SKUListSidebar from "./sku-list-sidebar";
|
||||||
|
|
||||||
|
export interface IDataSessionProps {
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DataSession({ data }: IDataSessionProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="col-span-8 display flex flex-col gap-10">
|
||||||
|
{data?.data?.length > 0 &&
|
||||||
|
(data.data || []).map((item: ItemType, index: number) => {
|
||||||
|
return <Item key={item?.sku || index} data={item} />;
|
||||||
|
})}
|
||||||
|
|
||||||
|
{data?.data?.length <= 0 && (
|
||||||
|
<Empty>
|
||||||
|
<EmptyHeader>
|
||||||
|
<EmptyMedia variant="icon">
|
||||||
|
<Box />
|
||||||
|
</EmptyMedia>
|
||||||
|
<EmptyTitle>No Products</EmptyTitle>
|
||||||
|
</EmptyHeader>
|
||||||
|
</Empty>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-4 -my-2 border-l">
|
||||||
|
<div className="sticky z-50 top-0">
|
||||||
|
<SKUListSidebar data={data?.data} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -23,17 +23,10 @@ import { Input } from "../ui/input";
|
||||||
import { Spinner } from "../ui/spinner";
|
import { Spinner } from "../ui/spinner";
|
||||||
import { Textarea } from "../ui/textarea";
|
import { Textarea } from "../ui/textarea";
|
||||||
import UncontrolledJoditEditor from "./uncontrolled-jodit-editor";
|
import UncontrolledJoditEditor from "./uncontrolled-jodit-editor";
|
||||||
|
import { useItemStore } from "@/stores/item-store";
|
||||||
|
|
||||||
export interface IItemProps {
|
export interface IItemProps {
|
||||||
data: Pick<
|
data: ItemType;
|
||||||
Sku,
|
|
||||||
| "id"
|
|
||||||
| "normalized_title"
|
|
||||||
| "normalized_short_description"
|
|
||||||
| "normalized_html"
|
|
||||||
| "sku"
|
|
||||||
| "status"
|
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemSchema = z.object({
|
const ItemSchema = z.object({
|
||||||
|
|
@ -50,6 +43,8 @@ type ItemFormValues = z.infer<typeof ItemSchema>;
|
||||||
export default function Item({ data }: IItemProps) {
|
export default function Item({ data }: IItemProps) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const { markUpdated } = useItemStore();
|
||||||
|
|
||||||
const form = useForm<ItemFormValues>({
|
const form = useForm<ItemFormValues>({
|
||||||
resolver: zodResolver(ItemSchema),
|
resolver: zodResolver(ItemSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|
@ -65,6 +60,7 @@ export default function Item({ data }: IItemProps) {
|
||||||
const res = await axios.get(`skus/${data.id}`);
|
const res = await axios.get(`skus/${data.id}`);
|
||||||
if (res?.data) {
|
if (res?.data) {
|
||||||
form.reset(res.data);
|
form.reset(res.data);
|
||||||
|
markUpdated(res.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { useItemStore } from "@/stores/item-store";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export interface INavItemProps {
|
||||||
|
item: ItemType;
|
||||||
|
active?: boolean;
|
||||||
|
onClick?: (data: ItemType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavItem({ item, active, onClick }: INavItemProps) {
|
||||||
|
const { updatedItems } = useItemStore();
|
||||||
|
|
||||||
|
const dyData = useMemo(() => {
|
||||||
|
const i = updatedItems.find((j) => j.id === item.id);
|
||||||
|
|
||||||
|
if (!i) return item;
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}, [updatedItems, item]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault(); // chặn default jump
|
||||||
|
// setSelectedSKU(item.sku);
|
||||||
|
onClick?.(dyData);
|
||||||
|
const el = document.getElementById(dyData.sku);
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`group flex items-start gap-3 px-3 py-3 rounded-lg transition-colors cursor-pointer ${
|
||||||
|
// selectedSKU === item.sku
|
||||||
|
active
|
||||||
|
? "bg-primary/10 border border-primary/20"
|
||||||
|
: "hover:bg-muted border border-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* SKU Badge */}
|
||||||
|
<div className="inline-flex items-center justify-center px-3 py-1 rounded-md bg-primary/20">
|
||||||
|
<span className="text-xs font-semibold text-primary truncate">
|
||||||
|
{dyData.sku}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-sm font-medium text-foreground truncate">
|
||||||
|
{dyData.normalized_title}
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-1 rounded-full whitespace-nowrap capitalize ${
|
||||||
|
dyData.status === "pass"
|
||||||
|
? "bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-400"
|
||||||
|
: "bg-gray-100 text-gray-700 dark:bg-gray-900 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{dyData.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground truncate mt-1">
|
||||||
|
{dyData.normalized_short_description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chevron */}
|
||||||
|
<ChevronRight className="w-4 h-4 text-muted-foreground group-hover:text-foreground mt-1 transition-colors" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Sku } from "@/entities/Sku";
|
|
||||||
import { ChevronRight, Search, X } from "lucide-react";
|
import { ChevronRight, Search, X } from "lucide-react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
import { Spinner } from "../ui/spinner";
|
import { Spinner } from "../ui/spinner";
|
||||||
|
import NavItem from "./nav-item";
|
||||||
|
|
||||||
export default function SKUListSidebar({
|
export default function SKUListSidebar({ data }: { data: ItemType[] }) {
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
data: Pick<
|
|
||||||
Sku,
|
|
||||||
| "id"
|
|
||||||
| "normalized_title"
|
|
||||||
| "normalized_short_description"
|
|
||||||
| "sku"
|
|
||||||
| "status"
|
|
||||||
>[];
|
|
||||||
}) {
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [selectedSKU, setSelectedSKU] = useState<string | null>(null);
|
const [selectedSKU, setSelectedSKU] = useState<string | null>(null);
|
||||||
|
|
@ -78,56 +67,12 @@ export default function SKUListSidebar({
|
||||||
<nav className="p-2 space-y-1">
|
<nav className="p-2 space-y-1">
|
||||||
{data.length > 0 ? (
|
{data.length > 0 ? (
|
||||||
data.map((item) => (
|
data.map((item) => (
|
||||||
<div
|
<NavItem
|
||||||
key={item.sku}
|
key={item.sku}
|
||||||
onClick={(e) => {
|
active={selectedSKU === item.sku}
|
||||||
e.preventDefault(); // chặn default jump
|
item={item}
|
||||||
setSelectedSKU(item.sku);
|
onClick={(i) => setSelectedSKU(i.sku)}
|
||||||
const el = document.getElementById(item.sku);
|
/>
|
||||||
if (el) {
|
|
||||||
el.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "center",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`group flex items-start gap-3 px-3 py-3 rounded-lg transition-colors cursor-pointer ${
|
|
||||||
selectedSKU === item.sku
|
|
||||||
? "bg-primary/10 border border-primary/20"
|
|
||||||
: "hover:bg-muted border border-transparent"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{/* SKU Badge */}
|
|
||||||
<div className="inline-flex items-center justify-center px-3 py-1 rounded-md bg-primary/20">
|
|
||||||
<span className="text-xs font-semibold text-primary truncate">
|
|
||||||
{item.sku}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="text-sm font-medium text-foreground truncate">
|
|
||||||
{item.normalized_title}
|
|
||||||
</p>
|
|
||||||
<span
|
|
||||||
className={`text-xs px-2 py-1 rounded-full whitespace-nowrap capitalize ${
|
|
||||||
item.status === "pass"
|
|
||||||
? "bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-400"
|
|
||||||
: "bg-gray-100 text-gray-700 dark:bg-gray-900 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground truncate mt-1">
|
|
||||||
{item.normalized_short_description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chevron */}
|
|
||||||
<ChevronRight className="w-4 h-4 text-muted-foreground group-hover:text-foreground mt-1 transition-colors" />
|
|
||||||
</div>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="px-3 py-8 text-center">
|
<div className="px-3 py-8 text-center">
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button"
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -17,7 +17,7 @@ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
className={cn("mx-auto flex w-full justify-center", className)}
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationContent({
|
function PaginationContent({
|
||||||
|
|
@ -30,17 +30,17 @@ function PaginationContent({
|
||||||
className={cn("flex flex-row items-center gap-1", className)}
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
return <li data-slot="pagination-item" {...props} />
|
return <li data-slot="pagination-item" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginationLinkProps = {
|
type PaginationLinkProps = {
|
||||||
isActive?: boolean
|
isActive?: boolean;
|
||||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
React.ComponentProps<"a">
|
React.ComponentProps<"a">;
|
||||||
|
|
||||||
function PaginationLink({
|
function PaginationLink({
|
||||||
className,
|
className,
|
||||||
|
|
@ -62,7 +62,7 @@ function PaginationLink({
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationPrevious({
|
function PaginationPrevious({
|
||||||
|
|
@ -79,7 +79,7 @@ function PaginationPrevious({
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
<span className="hidden sm:block">Previous</span>
|
<span className="hidden sm:block">Previous</span>
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationNext({
|
function PaginationNext({
|
||||||
|
|
@ -96,7 +96,7 @@ function PaginationNext({
|
||||||
<span className="hidden sm:block">Next</span>
|
<span className="hidden sm:block">Next</span>
|
||||||
<ChevronRightIcon />
|
<ChevronRightIcon />
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationEllipsis({
|
function PaginationEllipsis({
|
||||||
|
|
@ -113,7 +113,7 @@ function PaginationEllipsis({
|
||||||
<MoreHorizontalIcon className="size-4" />
|
<MoreHorizontalIcon className="size-4" />
|
||||||
<span className="sr-only">More pages</span>
|
<span className="sr-only">More pages</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -124,4 +124,4 @@ export {
|
||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
PaginationNext,
|
PaginationNext,
|
||||||
PaginationEllipsis,
|
PaginationEllipsis,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface ItemStore {
|
||||||
|
updatedItems: ItemType[];
|
||||||
|
markUpdated: (item: ItemType) => void;
|
||||||
|
clearUpdated: () => void;
|
||||||
|
removeItem: (id: string | number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useItemStore = create<ItemStore>((set) => ({
|
||||||
|
updatedItems: [],
|
||||||
|
markUpdated: (item: ItemType) =>
|
||||||
|
set((state) => {
|
||||||
|
const existIndex = state.updatedItems.findIndex((i) => i.id === item.id);
|
||||||
|
if (existIndex > -1) {
|
||||||
|
// update item hiện tại
|
||||||
|
const updatedItems = [...state.updatedItems];
|
||||||
|
updatedItems[existIndex] = item;
|
||||||
|
return { updatedItems };
|
||||||
|
}
|
||||||
|
// nếu chưa có thì thêm mới
|
||||||
|
return { updatedItems: [...state.updatedItems, item] };
|
||||||
|
}),
|
||||||
|
clearUpdated: () => set({ updatedItems: [] }),
|
||||||
|
removeItem: (id: string | number) =>
|
||||||
|
set((state) => ({
|
||||||
|
updatedItems: state.updatedItems.filter((i) => i.id !== id),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
type ItemType = Pick<
|
||||||
|
Sku,
|
||||||
|
| "id"
|
||||||
|
| "normalized_title"
|
||||||
|
| "normalized_short_description"
|
||||||
|
| "sku"
|
||||||
|
| "normalized_html"
|
||||||
|
| "status"
|
||||||
|
>;
|
||||||
Loading…
Reference in New Issue