Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BwanikaRobert this error is still being shown using toast, check the useAddMaintenanceLog hook and move the toasts to the UI side
Image

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { useUserContext } from "@/core/hooks/useUserContext";
import { MultiSelectCombobox } from "@/components/ui/multi-select";
import { Switch } from "@/components/ui/switch";
import ReusableDialog from "@/components/shared/dialog/ReusableDialog";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { useBanner } from "@/context/banner-context";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";

interface AddMaintenanceLogModalProps {
open: boolean;
Expand Down Expand Up @@ -51,6 +52,7 @@ const AddMaintenanceLogModal: React.FC<AddMaintenanceLogModalProps> = ({ open, o
const [maintenanceType, setMaintenanceType] = useState<"preventive" | "corrective">("preventive");

const { userDetails } = useUserContext();
const { showBanner } = useBanner();
const addMaintenanceLog = useAddMaintenanceLog();

// Reset form when modal opens/closes
Expand All @@ -65,7 +67,7 @@ const AddMaintenanceLogModal: React.FC<AddMaintenanceLogModalProps> = ({ open, o

const handleSubmit = async () => {
if (!date || selectedTags.length === 0) {
ReusableToast({message: "Please fill all fields", type:"ERROR"})
showBanner({ severity: 'error', message: 'Please fill all required fields', scoped: true });
return;
}

Expand All @@ -81,8 +83,13 @@ const AddMaintenanceLogModal: React.FC<AddMaintenanceLogModalProps> = ({ open, o
user_id: userDetails?._id || "",
};

await addMaintenanceLog.mutateAsync({ deviceName, logData });
onOpenChange(false);
try {
await addMaintenanceLog.mutateAsync({ deviceName, logData });
showBanner({ severity: 'success', message: `Maintenance log has been added for ${deviceName}.`, scoped: false });
onOpenChange(false);
} catch (error) {
showBanner({ severity: 'error', message: `Failed to Add Maintenance Log: ${getApiErrorMessage(error)}`, scoped: true });
}
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ReusableSelectInput from "@/components/shared/select/ReusableSelectInput"
import { MultiSelectCombobox } from "@/components/ui/multi-select";
import { DEFAULT_DEVICE_TAGS } from "@/core/constants/devices";
import { Label } from "@/components/ui/label";
import { useBanner } from "@/context/banner-context";

interface CreateDeviceModalProps {
open: boolean;
Expand All @@ -30,6 +31,7 @@ const CreateDeviceModal: React.FC<CreateDeviceModalProps> = ({
});

const [errors, setErrors] = useState<Record<string, string>>({});
const { showBanner } = useBanner();
const activeNetwork = useAppSelector((state) => state.user.activeNetwork);
const createDevice = useCreateDevice();

Expand All @@ -56,7 +58,7 @@ const CreateDeviceModal: React.FC<CreateDeviceModalProps> = ({
const effectiveNetworkName = networkName || activeNetwork?.net_name;

if (!effectiveNetworkName) {
setErrors({ general: "No active Sensor Manufacturer found" });
showBanner({ severity: 'error', message: 'No active Sensor Manufacturer found', scoped: true });
return;
}

Expand Down Expand Up @@ -136,11 +138,6 @@ const CreateDeviceModal: React.FC<CreateDeviceModalProps> = ({
}}
>
<div className="space-y-4">
{errors.general && (
<div className="text-sm text-red-600 bg-red-50 p-2 rounded">
{errors.general}
</div>
)}
<ReusableInputField
label="Device Name"
id="long_name"
Expand Down
38 changes: 24 additions & 14 deletions src/vertex/components/features/devices/deploy-device-component.tsx

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After device is deployed, toast is still being used. Move toast from useDeployDevice hook to UI component

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Codebmk In useDevices.ts should i replace all the ReusableToast components or only those you have so far mentioned?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shall do this following a separate issue and pull request. For now do these and just ensure that for all you've worked on so far they are working as expected @BwanikaRobert

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import { useUserContext } from "@/core/hooks/useUserContext";
import { useDeviceDetails, useDevices, useDeployDevice } from "@/core/hooks/useDevices";
import { ComboBox } from "@/components/ui/combobox";
import { Device, type DevicePreviousSite } from "@/app/types/devices";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { useBanner } from "@/context/banner-context";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import LocationAutocomplete from "@/components/features/location-autocomplete/LocationAutocomplete";
import { useNetworks } from "@/core/hooks/useNetworks";
const MiniMap = React.lazy(() => import("../mini-map/mini-map"));
Expand Down Expand Up @@ -427,6 +428,7 @@ const DeployDeviceComponent = ({
onDeploymentError
}: DeployDeviceComponentProps) => {
const queryClient = useQueryClient();
const { showBanner } = useBanner();
const { userScope, userDetails } = useUserContext();
const { devices: allDevices } = useDevices({ enabled: userScope !== 'personal' });
const [currentStep, setCurrentStep] = React.useState<number>(0);
Expand Down Expand Up @@ -608,10 +610,7 @@ const DeployDeviceComponent = ({

const handleNext = (): void => {
if (currentStep === 0 && !validateDeviceDetails()) {
ReusableToast({
type: "ERROR",
message: "Incomplete Details: Please fill in all required device details.",
});
showBanner({ severity: 'error', message: 'Incomplete Details: Please fill in all required device details.', scoped: true });
return;
}
setCurrentStep((prev) => Math.min(prev + 1, siteSource === 'new' ? 2 : 1));
Expand Down Expand Up @@ -642,18 +641,12 @@ const DeployDeviceComponent = ({

const handleDeploy = (): void => {
if (!userDetails?._id) {
ReusableToast({
type: "ERROR",
message: "User information not available. Please reload the page.",
});
showBanner({ severity: 'error', message: 'User information not available. Please reload the page.', scoped: true });
return;
}

if (siteSource === 'previous' && !deviceData.site_id) {
ReusableToast({
type: "ERROR",
message: "Select a previous site to continue.",
});
showBanner({ severity: 'error', message: 'Select a previous site to continue.', scoped: true });
return;
}

Expand Down Expand Up @@ -686,7 +679,8 @@ const DeployDeviceComponent = ({
queryClient.invalidateQueries({ queryKey: ["device-details", prefilledDevice._id] });
}

// On successful deployment, reset form fields
showBanner({ severity: 'success', message: `${deviceData.deviceName} has been deployed.`, scoped: false });

setDeviceData({
deviceName: "",
deployment_date: undefined,
Expand All @@ -709,6 +703,22 @@ const DeployDeviceComponent = ({
if (onClose) onClose();
},
onError: (error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errorData = (error as any)?.response?.data;
let errorMessage = getApiErrorMessage(error);

if (errorData?.failed_deployments?.length > 0) {
const failedMessages = errorData.failed_deployments
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((deployment: any) => deployment.error?.message)
.filter(Boolean);

if (failedMessages.length > 0) {
errorMessage = failedMessages.join(', ');
}
}

showBanner({ severity: 'error', message: `Deployment Failed: ${errorMessage}`, scoped: true });
onDeploymentError?.(error instanceof Error ? error : new Error("Unknown error"));
},
}
Expand Down
20 changes: 14 additions & 6 deletions src/vertex/components/features/devices/device-assignment-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { Button } from "@/components/ui/button";
import { useAppSelector } from "@/core/redux/hooks";
import { Device } from "@/app/types/devices";
import { useAssignDeviceToOrganization } from "@/core/hooks/useDevices";
import { useBanner } from "@/context/banner-context";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import { Label } from "@/components/ui/label";
import { ComboBox } from "@/components/ui/combobox";
import { useRouter } from "next/navigation";
Expand Down Expand Up @@ -45,16 +47,22 @@ const DeviceAssignmentModal: React.FC<DeviceAssignmentModalProps> = ({
const [selectedDevice, setSelectedDevice] = useState("");

const assignDevice = useAssignDeviceToOrganization();
const { showBanner } = useBanner();

const handleAssign = async () => {
if (!selectedOrganization || !selectedDevice || !userDetails?._id) return;

await assignDevice.mutateAsync({
device_name: selectedDevice,
organization_id: selectedOrganization,
user_id: userDetails._id,
});
onSuccess();
try {
const data = await assignDevice.mutateAsync({
device_name: selectedDevice,
organization_id: selectedOrganization,
user_id: userDetails._id,
});
showBanner({ severity: 'success', message: `${data.device.name} has been assigned to the organization.`, scoped: false });
onSuccess();
} catch (error) {
showBanner({ severity: 'error', message: `Assignment Failed: ${getApiErrorMessage(error)}`, scoped: false });
}
};

const handleClose = () => {
Expand Down
29 changes: 20 additions & 9 deletions src/vertex/components/features/devices/device-details-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import ReusableButton from "@/components/shared/button/ReusableButton";
import ReusableSelectInput from "@/components/shared/select/ReusableSelectInput";
import ReusableInputField from "@/components/shared/inputfield/ReusableInputField";
import { AqEdit01 as AqEdit, AqKey01 } from "@airqo/icons-react";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { useBanner } from "@/context/banner-context";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import logger from "@/lib/logger";

interface DeviceDetailsModalProps {
Expand Down Expand Up @@ -60,6 +61,7 @@ const DeviceDetailsModal: React.FC<DeviceDetailsModalProps> = ({ open, device, o
const updateLocal = useUpdateDeviceLocal();
const updateGlobal = useUpdateDeviceGlobal();
const decryptKeys = useDecryptDeviceKeys();
const { showBanner } = useBanner();

const form = useForm<DeviceUpdateFormData>({
resolver: zodResolver(deviceUpdateSchema),
Expand Down Expand Up @@ -132,7 +134,7 @@ const DeviceDetailsModal: React.FC<DeviceDetailsModalProps> = ({ open, device, o

const onSubmitLocal = async (data: DeviceUpdateFormData) => {
if (!device?._id) {
ReusableToast({ message: "Cannot update device: missing device ID", type: "WARNING" });
showBanner({ severity: 'warning', message: "Cannot update device: missing device ID", scoped: true });
return;
}

Expand Down Expand Up @@ -168,16 +170,20 @@ const DeviceDetailsModal: React.FC<DeviceDetailsModalProps> = ({ open, device, o
{ deviceId: device._id, deviceData: processedData },
{
onSuccess: () => {
showBanner({ severity: 'success', message: 'Device information has been updated locally.', scoped: true });
setIsEditMode(false);
form.reset(data);
},
onError: (error) => {
showBanner({ severity: 'error', message: `Update Failed: ${getApiErrorMessage(error)}`, scoped: true });
},
}
);
};

const onSubmitGlobal = async (data: DeviceUpdateFormData) => {
if (!device?._id) {
ReusableToast({ message: "Cannot update device: missing device ID", type: "WARNING" });
showBanner({ severity: 'warning', message: "Cannot update device: missing device ID", scoped: true });
return;
}

Expand Down Expand Up @@ -213,9 +219,13 @@ const DeviceDetailsModal: React.FC<DeviceDetailsModalProps> = ({ open, device, o
{ deviceId: device._id, deviceData: processedData },
{
onSuccess: () => {
showBanner({ severity: 'success', message: 'Device information has been updated globally.', scoped: true });
setIsEditMode(false);
form.reset(data);
},
onError: (error) => {
showBanner({ severity: 'error', message: `Sync Failed: ${getApiErrorMessage(error)}`, scoped: true });
},
}
);
};
Expand All @@ -224,9 +234,9 @@ const DeviceDetailsModal: React.FC<DeviceDetailsModalProps> = ({ open, device, o
if (valueToCopy !== undefined && valueToCopy !== null) {
try {
await navigator.clipboard?.writeText(String(valueToCopy))
ReusableToast({ message: "Decrypted Key Copied", type: "SUCCESS" })
showBanner({ severity: 'success', message: "Decrypted Key Copied", scoped: true });
} catch (err) {
ReusableToast({ message: `Failed to copy key: ${String(err)}`, type: "ERROR" })
showBanner({ severity: 'error', message: `Failed to copy key: ${String(err)}`, scoped: true });
}
}
}
Expand All @@ -236,7 +246,7 @@ const DeviceDetailsModal: React.FC<DeviceDetailsModalProps> = ({ open, device, o
const deviceNumber = device.device_number;

if (!encryptedKey || !deviceNumber) {
ReusableToast({ message: `No ${keyType} or device number available for decryption.`, type: "WARNING" });
showBanner({ severity: 'warning', message: `No ${keyType} or device number available for decryption.`, scoped: true });
return;
}

Expand All @@ -251,10 +261,11 @@ const DeviceDetailsModal: React.FC<DeviceDetailsModalProps> = ({ open, device, o
if (response.success && response.decrypted_keys && response.decrypted_keys.length > 0) {
handleCopy(response.decrypted_keys[0].decrypted_key);
} else {
ReusableToast({ message: "Decryption failed or no key returned.", type: "ERROR" });
showBanner({ severity: 'error', message: "Decryption failed or no key returned.", scoped: true });
}
} catch {
logger.info("Decryption failed"); // simple log
} catch (error) {
showBanner({ severity: 'error', message: `Decryption Failed: ${getApiErrorMessage(error)}`, scoped: true });
logger.info("Decryption failed");
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useGroupCohorts } from "@/core/hooks/useCohorts";
import { useAppSelector } from "@/core/redux/hooks";
import { usePathname } from "next/navigation";
import logger from "@/lib/logger";
import { useBanner } from "@/context/banner-context";
import { NetworkRequestDialog } from "../networks/network-request-dialog";
import { MultiSelectCombobox } from "@/components/ui/multi-select";
import { DEFAULT_DEVICE_TAGS } from "@/core/constants/devices";
Expand Down Expand Up @@ -45,6 +46,7 @@ const ImportDeviceModal: React.FC<ImportDeviceModalProps> = ({
const [showMore, setShowMore] = useState(false);
const [isRequestDialogOpen, setIsRequestDialogOpen] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const { showBanner } = useBanner();
const importDevice = useImportDevice();
const { networks, isLoading: isLoadingNetworks } = useNetworks();

Expand Down Expand Up @@ -108,6 +110,7 @@ const ImportDeviceModal: React.FC<ImportDeviceModalProps> = ({

if (!userId) {
logger.warn("User ID is missing");
showBanner({ severity: 'error', message: 'Unable to identify user. Please reload and try again.', scoped: true });
return;
}

Expand Down Expand Up @@ -180,11 +183,6 @@ const ImportDeviceModal: React.FC<ImportDeviceModalProps> = ({
}}
>
<div className="space-y-2">
{errors.general && (
<div className="text-sm text-red-600 bg-red-50 p-2 rounded">
{errors.general}
</div>
)}

<ReusableInputField
label="Device Name"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import ReusableDialog from "@/components/shared/dialog/ReusableDialog";
import ReusableSelectInput from "@/components/shared/select/ReusableSelectInput";
import { useRecallDevice } from "@/core/hooks/useDevices";
import { useUserContext } from "@/core/hooks/useUserContext";
import { useBanner } from "@/context/banner-context";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";

interface RecallDeviceDialogProps {
open: boolean;
Expand All @@ -27,6 +29,7 @@ export default function RecallDeviceDialog({
const [recallType, setRecallType] = useState<string>("");
const recallDevice = useRecallDevice();
const { userDetails } = useUserContext();
const { showBanner } = useBanner();

const handleRecall = async () => {
if (!recallType || !userDetails?._id) {
Expand All @@ -47,12 +50,11 @@ export default function RecallDeviceDialog({
},
});

// Reset form and close dialog
showBanner({ severity: 'success', message: `${deviceDisplayName || deviceName} has been recalled.`, scoped: true });
setRecallType("");
onOpenChange(false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
} catch (error) {
// Error handling is done in the hook
console.error("Recall failed:", error);
showBanner({ severity: 'error', message: `Recall Failed: ${getApiErrorMessage(error)}`, scoped: true });
}
};

Expand Down
Loading