Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#187419126 A seller should be able to update & delete a product item from their collection #14

Merged
merged 1 commit into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ module.exports = {
"react/no-unescaped-entities": "off",
'@typescript-eslint/no-explicit-any': 0,
"react/prop-types": "off",
"jsx-a11y/control-has-associated-label": "off",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
Expand Down
7 changes: 2 additions & 5 deletions src/__test__/addProducts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@ describe("FileUpload component", () => {
render(
<FileUpload onDrop={onDropMock} remove={removeMock} files={filesMock} />,
);

expect(screen.getByText(/example.png/)).toBeInTheDocument();
expect(screen.getByText(/example.png/)).toHaveClass("text-gray-500");
expect(screen.getByRole("button", { name: /remove/i })).toBeInTheDocument();
});

Expand Down Expand Up @@ -158,11 +155,11 @@ describe("test seller dashboard components", () => {

fireEvent.click(select);

const categoryOption = screen.getByText("Category 1");
const categoryOption = screen.getByText("Select a category");
expect(categoryOption).toBeDefined();

fireEvent.click(categoryOption);
expect(select).toHaveTextContent("Category 1");
expect(select).toHaveTextContent("Select a category");
});

it("should render the TextInput with label, placeholder, and no error", () => {
Expand Down
101 changes: 101 additions & 0 deletions src/__test__/productActions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import "@testing-library/jest-dom";
import {
fireEvent, render, screen, waitFor,
} from "@testing-library/react";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";

import store from "../redux/store";
import { fetchProducts } from "../redux/reducers/productsSlice";

jest.mock("react-dropzone", () => ({
useDropzone: jest.fn(),
}));

jest.mock("../redux/api/productsApiSlice", () => ({
addProduct: jest.fn(),
updateProduct: jest.fn(),
deleteProduct: jest.fn(),
}));

jest.mock("react-toastify", () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));

jest.mock("../components/dashboard/ConfirmModal", () => () => (
<div>ConfirmModal</div>
));
jest.mock("../components/dashboard/Spinner", () => () => <div>Spinner</div>);
jest.mock(
"../components/dashboard/ToggleSwitch",
() => ({ checked, onChange }) => (
<div onClick={onChange}>
ToggleSwitch
{checked ? "On" : "Off"}
</div>
),
);

const mockProducts = [
{
id: 1,
name: "Product 1",
stockQuantity: 10,
expiryDate: "2024-12-31T00:00:00.000Z",
price: 100,
category: { name: "Electronics" },
isAvailable: true,
images: ["image1.png"],
},
{
id: 2,
name: "Product 2",
stockQuantity: 5,
expiryDate: "2025-06-30T00:00:00.000Z",
price: 50,
category: { name: "Fashion" },
isAvailable: true,
images: ["image2.png"],
},
];

global.URL.createObjectURL = jest.fn();

describe("Products slice tests", () => {
it("should handle products initial state", () => {
expect(store.getState().products).toEqual({
loading: false,
data: [],
error: null,
});
});

it("should handle products pending", () => {
store.dispatch(fetchProducts.pending(""));
expect(store.getState().products).toEqual({
loading: true,
data: [],
error: null,
});
});

it("should handle products fulfilled", () => {
const mockData = { message: "success" };
// @ts-ignore
store.dispatch(fetchProducts.fulfilled(mockData, "", {}));
expect(store.getState().products).toEqual({
loading: false,
data: mockData,
error: null,
});
});

it("should handle products fetch rejected", () => {
// @ts-ignore
store.dispatch(fetchProducts.rejected(null, "", {}));
expect(store.getState().products.error).toEqual("Rejected");
});
});
50 changes: 50 additions & 0 deletions src/components/dashboard/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";

interface ConfirmDeleteModalProps {
onConfirm: () => void;
onCancel: () => void;
message: string;
product: any;
loading: boolean;
}

const ConfirmModal: React.FC<ConfirmDeleteModalProps> = ({
onConfirm,
onCancel,
message,
product,
loading,
}) => (
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white py-8 px-6 rounded-lg duration-75 animate-fadeIn">
<div className="flex flex-col gap-2 mb-3">
<h2 className="text-lg">{message}</h2>
<p className="text-gray-700">
<strong className="text-black">
{product.name}
.
</strong>
{' '}
This can't be
undone
</p>
</div>
<div className="flex justify-end">
<button
className="border border-blue-700 text-blue-700 px-4 py-2 rounded mr-2"
onClick={onCancel}
>
Cancel
</button>
<button
className="bg-red-600 text-white px-4 py-2 rounded"
onClick={onConfirm}
>
{loading ? "Loading..." : "Delete"}
</button>
</div>
</div>
</div>
);

export default ConfirmModal;
16 changes: 8 additions & 8 deletions src/components/dashboard/CustomSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,28 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
testId,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(defaultValue);
const loading = useSelector((state: RootState) => state.categories.loading);

const handleOptionClick = (option) => {
setSelectedOption(option.name);
const handleOptionClick = (option: any) => {
setIsOpen(false);
onSelect(option);
if (option !== "Category") {
onSelect(option);
}
};

return (
<div className="relative min-w-40">
<div
className="appearance-none rounded-[8px] border-[0.5px] border-[#E5E5E5] px-4 py-3 bg-white text-dark-gray flex items-center justify-between text-md gap-2 border-dark-gray text-dark-gray cursor-pointer"
className="appearance-none rounded-[8px] border-[0.5px] border-[#E5E5E5] px-4 py-3 bg-white flex items-center justify-between text-md gap-2 text-black cursor-pointer"
onClick={() => setIsOpen(!isOpen)}
data-testid={testId}
>
{selectedOption}
{defaultValue}
{' '}
<FaAngleDown />
</div>
{isOpen && (
<div className="absolute category-options top-full h-64 overflow-y-scroll w-full left-0 z-10 dark:bg-secondary-black shadow rounded-[8px] py-3 bg-white text-[#161616] border-dark-gray mt-1">
<div className="absolute category-options top-full max-h-64 overflow-y-scroll w-full left-0 z-10 shadow rounded-[8px] py-3 bg-white text-[#161616] border-gray-800 mt-1">
{loading ? (
<div className="flex items-center justify-center select-none py-6">
loading categories...
Expand All @@ -52,7 +52,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
options.map((option) => (
<div
key={option.id}
className="px-4 py-2 text-dark-gray text-md hover:bg-[#F7F8FA] cursor-pointer"
className="px-4 py-2 text-gray-700 text-md hover:bg-[#F7F8FA] cursor-pointer"
onClick={() => handleOptionClick(option)}
>
{option.name}
Expand Down
86 changes: 59 additions & 27 deletions src/components/dashboard/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ interface FileUploadProps {
onDrop: (acceptedFiles: File[]) => void;
remove: (file: File, event: React.MouseEvent<HTMLButtonElement>) => void;
files: File[];
existingImages?: string[];
removeExistingImage: (
index: number,
image: string,
event: React.MouseEvent<HTMLButtonElement>,
) => void;
}

const FileUpload = forwardRef<HTMLInputElement, FileUploadProps>(
({ onDrop, remove, files }, ref) => {
({
onDrop, remove, files, existingImages, removeExistingImage,
}, ref) => {
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
Expand All @@ -22,38 +30,62 @@ const FileUpload = forwardRef<HTMLInputElement, FileUploadProps>(
return (
<div
{...getRootProps()}
className={`border-dashed border-2 rounded-[8px] p-4 flex flex-col bg-white text-primary justify-center items-center cursor-pointer ${
className={`border-dashed border-2 rounded-[8px] p-4 flex flex-col bg-white text-primary justify-center items-center cursor-pointer ${
isDragActive ? "border-primary" : "border-[#687588]"
}`}
>
<input {...getInputProps()} ref={ref} />
{files.length > 0 ? (
files.map((file) => (
<div
key={file.name}
className="flex items-center w-full justify-between"
>
<img
src={URL.createObjectURL(file)}
alt="Preview"
className="mb-2 w-16 h-16 object-cover rounded-lg"
/>
<p className="text-gray-500">{`${file.name.slice(0, 15)}...`}</p>
<button
type="button"
onClick={(e) => remove(file, e)}
className="text-red-500"
<div className=" max-h-80 w-full overflow-y-scroll px-3">
{existingImages && existingImages.length > 0 && (
<>
{existingImages.map((image, index) => (
<div
key={index}
className="flex items-center w-full justify-between"
>
<img
src={image}
alt="Preview"
className="mb-2 w-16 h-16 object-cover rounded-lg"
/>
<button
type="button"
onClick={(e) => removeExistingImage(index, image, e)}
className="text-red-500"
>
Remove
</button>
</div>
))}
</>
)}
{files.length > 0 ? (
files.map((file, index) => (
<div
key={index}
className="flex items-center w-full justify-between"
>
Remove
</button>
<img
src={URL.createObjectURL(file)}
alt="Preview"
className="mb-2 w-16 h-16 object-cover rounded-lg"
/>
<button
type="button"
onClick={(e) => remove(file, e)}
className="text-red-500"
>
Remove
</button>
</div>
))
) : existingImages && existingImages.length > 0 ? null : (
<div className="flex items-center flex-col gap-2">
<FaImages className="text-gray-500 text-4xl mb-2" />
<p className="text-gray-500">Browse Images...</p>
</div>
))
) : (
<div className="flex items-center flex-col gap-2">
<FaImages className="text-gray-500 text-4xl mb-2" />
<p className="text-gray-500">Browse Images...</p>
</div>
)}
)}
</div>
</div>
);
},
Expand Down
Loading
Loading