Strip out partner related functionality.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Shop Partner</title>
|
||||
<title>EMS Uploader</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<!-- <meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
// src/renderer/src/App.test.tsx
|
||||
|
||||
// Mock data
|
||||
const mockUser = {
|
||||
uid: "test123",
|
||||
email: "test@example.com",
|
||||
displayName: "Test User",
|
||||
toJSON: () => ({
|
||||
uid: "test123",
|
||||
email: "test@example.com",
|
||||
displayName: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
test.describe("App Component", () => {
|
||||
let page: Page;
|
||||
|
||||
test.beforeEach(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
|
||||
// Mock Firebase Auth
|
||||
await page.addInitScript(() => {
|
||||
window.mockAuthState = null;
|
||||
|
||||
// Mock the firebase auth module
|
||||
jest.mock("./util/firebase", () => ({
|
||||
auth: {
|
||||
onAuthStateChanged: (callback) => {
|
||||
callback(window.mockAuthState);
|
||||
// Return mock unsubscribe function
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock electron IPC
|
||||
window.electron = {
|
||||
ipcRenderer: {
|
||||
send: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
});
|
||||
|
||||
test("should show SignInForm when user is not authenticated", async () => {
|
||||
await page.evaluate(() => {
|
||||
window.mockAuthState = null;
|
||||
});
|
||||
|
||||
await page.reload();
|
||||
|
||||
// Check if SignInForm is visible
|
||||
const signInForm = await page
|
||||
.locator("form")
|
||||
.filter({ hasText: "Sign In" });
|
||||
await expect(signInForm).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show routes when user is authenticated", async () => {
|
||||
await page.evaluate((user) => {
|
||||
window.mockAuthState = user;
|
||||
}, mockUser);
|
||||
|
||||
await page.reload();
|
||||
|
||||
// Check if AuthHome is visible
|
||||
const authHome = await page.locator('div:text("AuthHome")');
|
||||
await expect(authHome).toBeVisible();
|
||||
|
||||
// Check that electron IPC was called with auth state
|
||||
await expect(
|
||||
page.evaluate(() => {
|
||||
return window.electron.ipcRenderer.send.mock.calls.length > 0;
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
test("should navigate to settings page when authenticated", async () => {
|
||||
await page.evaluate((user) => {
|
||||
window.mockAuthState = user;
|
||||
}, mockUser);
|
||||
|
||||
await page.reload();
|
||||
|
||||
// Navigate to settings
|
||||
await page.click('a[href="/settings"]');
|
||||
|
||||
// Check if Settings page is visible
|
||||
const settingsPage = await page.locator('div:text("Settings")');
|
||||
await expect(settingsPage).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,44 +1,16 @@
|
||||
import "@ant-design/v5-patch-for-react-19";
|
||||
import { Layout, Skeleton, ConfigProvider, Badge } from "antd";
|
||||
import { User } from "firebase/auth";
|
||||
import { useEffect, useState, FC } from "react";
|
||||
import { Badge, ConfigProvider, Layout, Skeleton } from "antd";
|
||||
import { FC } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Provider } from "react-redux";
|
||||
import { HashRouter, Route, Routes } from "react-router";
|
||||
import ipcTypes from "../../util/ipcTypes.json";
|
||||
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback/ErrorBoundaryFallback";
|
||||
import Settings from "./components/Settings/Settings";
|
||||
import SignInForm from "./components/SignInForm/SignInForm";
|
||||
import UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable";
|
||||
import reduxStore from "./redux/redux-store";
|
||||
import { auth } from "./util/firebase";
|
||||
import { NotificationProvider } from "./util/notificationContext";
|
||||
|
||||
const App: FC = () => {
|
||||
const [user, setUser] = useState<User | boolean | null>(false);
|
||||
useEffect(() => {
|
||||
// Only set up the listener once when component mounts
|
||||
if (auth.currentUser) {
|
||||
setUser(auth.currentUser);
|
||||
} else {
|
||||
setUser(false);
|
||||
}
|
||||
const unsubscribe = auth.onAuthStateChanged((user: User | null) => {
|
||||
setUser(user);
|
||||
//Send back to the main process so that it knows we are authenticated.
|
||||
if (user) {
|
||||
window.electron.ipcRenderer.send(
|
||||
ipcTypes.toMain.authStateChanged,
|
||||
user.toJSON(),
|
||||
);
|
||||
window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start);
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up the listener when component unmounts
|
||||
return (): void => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const isTest = window.api.isTest();
|
||||
|
||||
return (
|
||||
@@ -58,23 +30,19 @@ const App: FC = () => {
|
||||
<HashRouter>
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<NotificationProvider>
|
||||
<Skeleton loading={user === false} active>
|
||||
<Skeleton loading={false} active>
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
{!user ? (
|
||||
<SignInForm />
|
||||
) : (
|
||||
<Badge.Ribbon
|
||||
text={isTest && "Connected to Test"}
|
||||
color={isTest ? "red" : undefined}
|
||||
>
|
||||
<Layout.Content style={{ padding: "0 24px" }}>
|
||||
<UpdateAvailable />
|
||||
<Routes>
|
||||
<Route path="/" element={<Settings />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Badge.Ribbon>
|
||||
)}
|
||||
<Badge.Ribbon
|
||||
text={isTest && "Connected to Test"}
|
||||
color={isTest ? "red" : undefined}
|
||||
>
|
||||
<Layout.Content style={{ padding: "0 24px" }}>
|
||||
<UpdateAvailable />
|
||||
<Routes>
|
||||
<Route path="/" element={<Settings />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Badge.Ribbon>
|
||||
</Layout>
|
||||
</Skeleton>
|
||||
</NotificationProvider>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { FC } from "react";
|
||||
|
||||
const Home: FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Home</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,164 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import ipcTypes from "../../../../../util/ipcTypes.json";
|
||||
import {
|
||||
PaintScaleConfig,
|
||||
PaintScaleType,
|
||||
} from "../../../../../util/types/paintScale";
|
||||
import { message } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ConfigType = "input" | "output";
|
||||
|
||||
export const usePaintScaleConfig = (configType: ConfigType) => {
|
||||
const [paintScaleConfigs, setPaintScaleConfigs] = useState<
|
||||
PaintScaleConfig[]
|
||||
>([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Get the appropriate IPC methods based on config type
|
||||
const getConfigsMethod =
|
||||
configType === "input"
|
||||
? ipcTypes.toMain.settings.paintScale.getInputConfigs
|
||||
: ipcTypes.toMain.settings.paintScale.getOutputConfigs;
|
||||
|
||||
const setConfigsMethod =
|
||||
configType === "input"
|
||||
? ipcTypes.toMain.settings.paintScale.setInputConfigs
|
||||
: ipcTypes.toMain.settings.paintScale.setOutputConfigs;
|
||||
|
||||
const setPathMethod =
|
||||
configType === "input"
|
||||
? ipcTypes.toMain.settings.paintScale.setInputPath
|
||||
: ipcTypes.toMain.settings.paintScale.setOutputPath;
|
||||
|
||||
// Load paint scale configs on mount
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer
|
||||
.invoke(getConfigsMethod)
|
||||
.then((configs: PaintScaleConfig[]) => {
|
||||
// Ensure all configs have a pollingInterval and type (for backward compatibility)
|
||||
const defaultPolling = configType === "input" ? 1440 : 60;
|
||||
const updatedConfigs = configs.map((config) => ({
|
||||
...config,
|
||||
pollingInterval: config.pollingInterval || defaultPolling, // Default to 1440 for input, 60 for output
|
||||
type: config.type || PaintScaleType.PPG, // Default type if missing
|
||||
}));
|
||||
setPaintScaleConfigs(updatedConfigs || []);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
`Failed to load paint scale ${configType} configs:`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
}, [getConfigsMethod]);
|
||||
|
||||
// Save configs to store and notify main process of config changes
|
||||
const saveConfigs = (configs: PaintScaleConfig[]) => {
|
||||
window.electron.ipcRenderer
|
||||
.invoke(setConfigsMethod, configs)
|
||||
.then(() => {
|
||||
// Notify main process to update cron job
|
||||
if (configType === "input") {
|
||||
window.electron.ipcRenderer.send(
|
||||
ipcTypes.toMain.settings.paintScale.updateInputCron,
|
||||
configs,
|
||||
);
|
||||
} else if (configType === "output") {
|
||||
window.electron.ipcRenderer.send(
|
||||
ipcTypes.toMain.settings.paintScale.updateOutputCron,
|
||||
configs,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
`Failed to save paint scale ${configType} configs:`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// New helper to check if a path is unique across input and output configs
|
||||
const checkPathUnique = async (newPath: string): Promise<boolean> => {
|
||||
try {
|
||||
const inputConfigs: PaintScaleConfig[] =
|
||||
await window.electron.ipcRenderer.invoke(
|
||||
ipcTypes.toMain.settings.paintScale.getInputConfigs,
|
||||
);
|
||||
const outputConfigs: PaintScaleConfig[] =
|
||||
await window.electron.ipcRenderer.invoke(
|
||||
ipcTypes.toMain.settings.paintScale.getOutputConfigs,
|
||||
);
|
||||
const allConfigs = [...inputConfigs, ...outputConfigs];
|
||||
// Allow updating the current config even if its current value equals newPath.
|
||||
return !allConfigs.some((config) => config.path === newPath);
|
||||
} catch (error) {
|
||||
console.error("Failed to check unique path:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle adding a new paint scale config
|
||||
const handleAddConfig = (type: PaintScaleType) => {
|
||||
const defaultPolling = configType === "input" ? 1440 : 60;
|
||||
const newConfig: PaintScaleConfig = {
|
||||
id: Date.now().toString(),
|
||||
type,
|
||||
pollingInterval: defaultPolling, // Default to 1440 for input, 60 for output
|
||||
};
|
||||
const updatedConfigs = [...paintScaleConfigs, newConfig];
|
||||
setPaintScaleConfigs(updatedConfigs);
|
||||
saveConfigs(updatedConfigs);
|
||||
};
|
||||
|
||||
// Handle removing a config
|
||||
const handleRemoveConfig = (id: string) => {
|
||||
const updatedConfigs = paintScaleConfigs.filter(
|
||||
(config) => config.id !== id,
|
||||
);
|
||||
setPaintScaleConfigs(updatedConfigs);
|
||||
saveConfigs(updatedConfigs);
|
||||
};
|
||||
|
||||
// Handle path selection (modified to check directory uniqueness)
|
||||
const handlePathChange = async (id: string) => {
|
||||
try {
|
||||
const path: string | null = await window.electron.ipcRenderer.invoke(
|
||||
setPathMethod,
|
||||
id,
|
||||
);
|
||||
if (path) {
|
||||
const isUnique = await checkPathUnique(path);
|
||||
if (!isUnique) {
|
||||
message.error(t("settings.errors.duplicatePath"));
|
||||
return;
|
||||
}
|
||||
const updatedConfigs = paintScaleConfigs.map((config) =>
|
||||
config.id === id ? { ...config, path } : config,
|
||||
);
|
||||
setPaintScaleConfigs(updatedConfigs);
|
||||
saveConfigs(updatedConfigs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to set paint scale ${configType} path:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle polling interval change
|
||||
const handlePollingIntervalChange = (id: string, pollingInterval: number) => {
|
||||
const updatedConfigs = paintScaleConfigs.map((config) =>
|
||||
config.id === id ? { ...config, pollingInterval } : config,
|
||||
);
|
||||
setPaintScaleConfigs(updatedConfigs);
|
||||
saveConfigs(updatedConfigs);
|
||||
};
|
||||
|
||||
return {
|
||||
paintScaleConfigs,
|
||||
handleAddConfig,
|
||||
handleRemoveConfig,
|
||||
handlePathChange,
|
||||
handlePollingIntervalChange,
|
||||
};
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { FolderOpenFilled } from "@ant-design/icons";
|
||||
import { Button, Card, Input, Space } from "antd";
|
||||
import { useEffect, useState, FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ipcTypes from "../../../../util/ipcTypes.json";
|
||||
|
||||
const SettingsEmsOutFilePath: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [emsFilePath, setEmsFilePath] = useState<string | null>(null);
|
||||
|
||||
const getPollingStateFromStore = (): void => {
|
||||
window.electron.ipcRenderer
|
||||
.invoke(ipcTypes.toMain.settings.getEmsOutFilePath)
|
||||
.then((filePath: string | null) => {
|
||||
setEmsFilePath(filePath);
|
||||
});
|
||||
};
|
||||
|
||||
//Get state first time it renders.
|
||||
useEffect(() => {
|
||||
getPollingStateFromStore();
|
||||
}, []);
|
||||
|
||||
const handlePathChange = (): void => {
|
||||
window.electron.ipcRenderer
|
||||
.invoke(ipcTypes.toMain.settings.setEmsOutFilePath)
|
||||
.then((filePath: string | null) => {
|
||||
setEmsFilePath(filePath);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("settings.labels.emsOutFilePath")}>
|
||||
<Space wrap>
|
||||
<Input
|
||||
value={emsFilePath || ""}
|
||||
placeholder={t("settings.labels.emsOutFilePath")}
|
||||
disabled
|
||||
/>
|
||||
<Button onClick={handlePathChange} icon={<FolderOpenFilled />} />
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export default SettingsEmsOutFilePath;
|
||||
@@ -1,183 +0,0 @@
|
||||
import {
|
||||
CheckCircleFilled,
|
||||
FileAddFilled,
|
||||
FolderOpenFilled,
|
||||
WarningFilled,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
theme,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import { JSX, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
PaintScaleConfig,
|
||||
PaintScaleType,
|
||||
paintScaleTypeOptions,
|
||||
} from "../../../../util/types/paintScale";
|
||||
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
|
||||
|
||||
const SettingsPaintScaleInputPaths = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { token } = theme.useToken(); // Access theme tokens
|
||||
|
||||
const {
|
||||
paintScaleConfigs,
|
||||
handleAddConfig,
|
||||
handleRemoveConfig,
|
||||
handlePathChange,
|
||||
handlePollingIntervalChange,
|
||||
} = usePaintScaleConfig("output");
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
|
||||
|
||||
// Show modal when adding a new path
|
||||
const showAddPathModal = () => {
|
||||
setSelectedType(null);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
// Handle modal confirmation
|
||||
const handleModalOk = () => {
|
||||
if (selectedType) {
|
||||
handleAddConfig(selectedType);
|
||||
setIsModalVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle modal cancellation
|
||||
const handleModalCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
// Table columns for paint scale configs
|
||||
const columns = [
|
||||
{
|
||||
title: t("settings.labels.paintScaleType"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
render: (type: PaintScaleType) => {
|
||||
const typeOption = paintScaleTypeOptions.find(
|
||||
(option) => option.value === type,
|
||||
);
|
||||
const label = typeOption ? typeOption.label : type;
|
||||
const colorMap: Partial<Record<PaintScaleType, string>> = {
|
||||
[PaintScaleType.PPG]: "blue",
|
||||
// Add other types and colors as needed
|
||||
};
|
||||
return <Tag color={colorMap[type] || "default"}>{label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("settings.labels.paintScalePath"),
|
||||
dataIndex: "path",
|
||||
key: "path",
|
||||
render: (path: string | null, record: PaintScaleConfig) => {
|
||||
const isValid = path && path.trim() !== "";
|
||||
return (
|
||||
<Space>
|
||||
<Input
|
||||
value={path || ""}
|
||||
placeholder={t("settings.labels.paintScalePath")}
|
||||
disabled
|
||||
style={{
|
||||
borderColor: isValid ? token.colorSuccess : token.colorError, // Use semantic tokens
|
||||
}}
|
||||
suffix={
|
||||
<Tooltip
|
||||
title={
|
||||
isValid
|
||||
? t("settings.labels.validPath")
|
||||
: t("settings.labels.invalidPath")
|
||||
}
|
||||
>
|
||||
{isValid ? (
|
||||
<CheckCircleFilled style={{ color: token.colorSuccess }} />
|
||||
) : (
|
||||
<WarningFilled style={{ color: token.colorError }} />
|
||||
)}
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handlePathChange(record.id)}
|
||||
icon={<FolderOpenFilled />}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("settings.labels.pollingInterval"),
|
||||
dataIndex: "pollingInterval",
|
||||
key: "pollingInterval",
|
||||
render: (pollingInterval: number, record: PaintScaleConfig) => (
|
||||
<Input
|
||||
type="number"
|
||||
value={pollingInterval}
|
||||
onChange={(e) =>
|
||||
handlePollingIntervalChange(record.id, Number(e.target.value))
|
||||
}
|
||||
style={{ width: 100 }}
|
||||
placeholder={t("settings.labels.pollingInterval")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("settings.labels.actions"),
|
||||
key: "actions",
|
||||
render: (_: any, record: PaintScaleConfig) => (
|
||||
<Button danger onClick={() => handleRemoveConfig(record.id)}>
|
||||
{t("settings.labels.remove")}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
title={t("settings.labels.paintScaleSettingsInput")}
|
||||
extra={
|
||||
<Button onClick={showAddPathModal} icon={<FileAddFilled />}>
|
||||
{t("settings.actions.addpath")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
dataSource={paintScaleConfigs}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={t("settings.labels.selectPaintScaleType")}
|
||||
open={isModalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={handleModalCancel}
|
||||
okButtonProps={{ disabled: !selectedType }}
|
||||
>
|
||||
<Select
|
||||
value={selectedType}
|
||||
options={paintScaleTypeOptions}
|
||||
onChange={(value) => setSelectedType(value)}
|
||||
style={{ width: "100%" }}
|
||||
placeholder={t("settings.labels.selectPaintScaleType")}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPaintScaleInputPaths;
|
||||
@@ -1,173 +0,0 @@
|
||||
import {
|
||||
CheckCircleFilled,
|
||||
FileAddFilled,
|
||||
FolderOpenFilled,
|
||||
WarningFilled,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
theme,
|
||||
} from "antd";
|
||||
import { JSX, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
PaintScaleConfig,
|
||||
PaintScaleType,
|
||||
paintScaleTypeOptions,
|
||||
} from "../../../../util/types/paintScale";
|
||||
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
|
||||
|
||||
const SettingsPaintScaleOutputPaths = (): JSX.Element => {
|
||||
const { token } = theme.useToken();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
paintScaleConfigs,
|
||||
handleAddConfig,
|
||||
handleRemoveConfig,
|
||||
handlePathChange,
|
||||
handlePollingIntervalChange,
|
||||
} = usePaintScaleConfig("input");
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
|
||||
|
||||
// Show modal when adding a new path
|
||||
const showAddPathModal = () => {
|
||||
setSelectedType(null);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
// Handle modal confirmation
|
||||
const handleModalOk = () => {
|
||||
if (selectedType) {
|
||||
handleAddConfig(selectedType);
|
||||
setIsModalVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle modal cancellation
|
||||
const handleModalCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
// Table columns for paint scale configs
|
||||
const columns = [
|
||||
{
|
||||
title: t("settings.labels.paintScaleType"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
render: (type: PaintScaleType) => {
|
||||
const typeOption = paintScaleTypeOptions.find(
|
||||
(option) => option.value === type,
|
||||
);
|
||||
const label = typeOption ? typeOption.label : type;
|
||||
const colorMap: Partial<Record<PaintScaleType, string>> = {
|
||||
[PaintScaleType.PPG]: "blue",
|
||||
// Add other types and colors as needed
|
||||
};
|
||||
return <Tag color={colorMap[type] || "default"}>{label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("settings.labels.paintScalePath"),
|
||||
dataIndex: "path",
|
||||
key: "path",
|
||||
render: (path: string | null, record: PaintScaleConfig) => {
|
||||
const isValid = path && path.trim() !== "";
|
||||
return (
|
||||
<Space>
|
||||
<Input
|
||||
value={path || ""}
|
||||
placeholder={t("settings.labels.paintScalePath")}
|
||||
disabled
|
||||
style={{
|
||||
borderColor: isValid ? token.colorSuccess : token.colorError,
|
||||
}}
|
||||
suffix={
|
||||
isValid ? (
|
||||
<CheckCircleFilled style={{ color: token.colorSuccess }} />
|
||||
) : (
|
||||
<WarningFilled style={{ color: token.colorError }} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handlePathChange(record.id)}
|
||||
icon={<FolderOpenFilled />}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("settings.labels.pollingInterval"),
|
||||
dataIndex: "pollingInterval",
|
||||
key: "pollingInterval",
|
||||
render: (pollingInterval: number, record: PaintScaleConfig) => (
|
||||
<Input
|
||||
type="number"
|
||||
value={pollingInterval}
|
||||
onChange={(e) =>
|
||||
handlePollingIntervalChange(record.id, Number(e.target.value))
|
||||
}
|
||||
style={{ width: 100 }}
|
||||
placeholder={t("settings.labels.pollingInterval")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("settings.labels.actions"),
|
||||
key: "actions",
|
||||
render: (_: any, record: PaintScaleConfig) => (
|
||||
<Button danger onClick={() => handleRemoveConfig(record.id)}>
|
||||
{t("settings.labels.remove")}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
title={t("settings.labels.paintScaleSettingsOutput")}
|
||||
extra={
|
||||
<Button onClick={showAddPathModal} icon={<FileAddFilled />}>
|
||||
{t("settings.actions.addpath")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
dataSource={paintScaleConfigs}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={t("settings.labels.selectPaintScaleType")}
|
||||
open={isModalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={handleModalCancel}
|
||||
okButtonProps={{ disabled: !selectedType }}
|
||||
>
|
||||
<Select
|
||||
value={selectedType}
|
||||
options={paintScaleTypeOptions}
|
||||
onChange={(value) => setSelectedType(value)}
|
||||
style={{ width: "100%" }}
|
||||
placeholder={t("settings.labels.selectPaintScaleType")}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPaintScaleOutputPaths;
|
||||
@@ -1,46 +0,0 @@
|
||||
import { FolderOpenFilled } from "@ant-design/icons";
|
||||
import { Button, Card, Input, Space } from "antd";
|
||||
import { useEffect, useState, FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ipcTypes from "../../../../util/ipcTypes.json";
|
||||
|
||||
const SettingsPpcFilepath: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [ppcFilePath, setPpcFilePath] = useState<string | null>(null);
|
||||
|
||||
const getPollingStateFromStore = (): void => {
|
||||
window.electron.ipcRenderer
|
||||
.invoke(ipcTypes.toMain.settings.getPpcFilePath)
|
||||
.then((filePath: string | null) => {
|
||||
setPpcFilePath(filePath);
|
||||
});
|
||||
};
|
||||
|
||||
//Get state first time it renders.
|
||||
useEffect(() => {
|
||||
getPollingStateFromStore();
|
||||
}, []);
|
||||
|
||||
const handlePathChange = (): void => {
|
||||
window.electron.ipcRenderer
|
||||
.invoke(ipcTypes.toMain.settings.setPpcFilePath)
|
||||
.then((filePath: string | null) => {
|
||||
setPpcFilePath(filePath);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("settings.labels.ppcfilepath")}>
|
||||
<Space wrap>
|
||||
<Input
|
||||
value={ppcFilePath || ""}
|
||||
placeholder={t("settings.labels.ppcfilepath")}
|
||||
disabled
|
||||
/>
|
||||
<Button onClick={handlePathChange} icon={<FolderOpenFilled />} />
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export default SettingsPpcFilepath;
|
||||
@@ -3,43 +3,23 @@ import { Col, Row } from "antd";
|
||||
import { FC } from "react";
|
||||
import SettingsWatchedPaths from "./Settings.WatchedPaths";
|
||||
import SettingsWatcher from "./Settings.Watcher";
|
||||
import Welcome from "../Welcome/Welcome";
|
||||
import SettingsPpcFilepath from "./Settings.PpcFilePath";
|
||||
import SettingsEmsOutFilePath from "./Settings.EmsOutFilePath";
|
||||
import SettingsPaintScaleInputPaths from "./Settings.PaintScaleInputPaths";
|
||||
import SettingsPaintScaleOutputPaths from "./Settings.PaintScaleOutputPaths";
|
||||
|
||||
const colSpans = {
|
||||
md: 12, // Two columns on medium screens and above
|
||||
sm: 24, // One column on small screens
|
||||
md: 12, // Two columns on medium screens and above
|
||||
sm: 24, // One column on small screens
|
||||
};
|
||||
|
||||
const Settings: FC = () => {
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Welcome />
|
||||
</Col>
|
||||
<Col {...colSpans}>
|
||||
<SettingsWatchedPaths />
|
||||
</Col>
|
||||
<Col {...colSpans}>
|
||||
<SettingsWatcher />
|
||||
</Col>
|
||||
<Col {...colSpans}>
|
||||
<SettingsPpcFilepath />
|
||||
</Col>
|
||||
<Col {...colSpans}>
|
||||
<SettingsEmsOutFilePath />
|
||||
</Col>
|
||||
<Col {...colSpans}>
|
||||
<SettingsPaintScaleInputPaths />
|
||||
</Col>
|
||||
<Col {...colSpans}>
|
||||
<SettingsPaintScaleOutputPaths />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col {...colSpans}>
|
||||
<SettingsWatchedPaths />
|
||||
</Col>
|
||||
<Col {...colSpans}>
|
||||
<SettingsWatcher />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
export default Settings;
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import { auth } from "@renderer/util/firebase";
|
||||
import type { FormProps } from "antd";
|
||||
import { Alert, Button, Card, Form, Input, Typography } from "antd";
|
||||
import log from "electron-log/renderer";
|
||||
import { signInWithEmailAndPassword } from "firebase/auth";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import errorTypeCheck from "../../../../util/errorTypeCheck";
|
||||
import ipcTypes from "../../../../util/ipcTypes.json";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
type FieldType = {
|
||||
username: string;
|
||||
password: string;
|
||||
remember?: string;
|
||||
};
|
||||
|
||||
const SignInForm: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const onFinish: FormProps<FieldType>["onFinish"] = async (values) => {
|
||||
const { username, password } = values;
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await signInWithEmailAndPassword(auth, username, password);
|
||||
log.debug("Login result", result);
|
||||
} catch (error) {
|
||||
log.error("Login error", errorTypeCheck(error));
|
||||
setError(t("auth.login.error"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (
|
||||
errorInfo,
|
||||
) => {
|
||||
log.log("Failed:", errorInfo);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
maxWidth: 600,
|
||||
margin: "auto auto",
|
||||
borderRadius: 8,
|
||||
paddingLeft: 48,
|
||||
paddingRight: 48,
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center", marginBottom: 24 }}>
|
||||
<Title level={2}>
|
||||
{import.meta.env.VITE_COMPANY === "IMEX"
|
||||
? t("title.imex")
|
||||
: t("title.rome")}
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
name="desktop-sign-in"
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
requiredMark={false}
|
||||
>
|
||||
{error && (
|
||||
<Form.Item>
|
||||
<Alert
|
||||
message={error}
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Username"
|
||||
name="username"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t(
|
||||
"auth.login.usernameRequired",
|
||||
"Please enter your username",
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Password"
|
||||
name="password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t(
|
||||
"auth.login.passwordRequired",
|
||||
"Please enter your password",
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
block
|
||||
>
|
||||
{t("auth.login.login")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: "center" }}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(): void => {
|
||||
window.electron.ipcRenderer.send(
|
||||
ipcTypes.toMain.user.resetPassword,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("auth.login.resetpassword")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignInForm;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { JSX, useState } from "react";
|
||||
|
||||
function Versions(): JSX.Element {
|
||||
const [versions] = useState(window.electron.process.versions);
|
||||
|
||||
return (
|
||||
<ul className="versions">
|
||||
<li className="electron-version">Electron v{versions.electron}</li>
|
||||
<li className="chrome-version">Chromium v{versions.chrome}</li>
|
||||
<li className="node-version">Node v{versions.node}</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default Versions;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { LogoutOutlined } from "@ant-design/icons";
|
||||
import { auth } from "@renderer/util/firebase";
|
||||
import { Button, Space, Typography } from "antd";
|
||||
import { isEmpty } from "lodash";
|
||||
import { JSX, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ipcTypes from "../../../../util/ipcTypes.json";
|
||||
|
||||
const Welcome = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [shopName, setShopName] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer
|
||||
.invoke(ipcTypes.toMain.user.getActiveShop)
|
||||
.then((shopName: string) => {
|
||||
console.log("Active shop name:", shopName);
|
||||
setShopName(shopName);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={4}>
|
||||
{t("auth.labels.welcome", {
|
||||
name: isEmpty(auth.currentUser?.displayName)
|
||||
? auth.currentUser?.email
|
||||
: `${auth.currentUser?.displayName} (${auth.currentUser?.email})`.trim(),
|
||||
})}
|
||||
</Typography.Title>
|
||||
<Space align="baseline">
|
||||
<Typography.Paragraph>{shopName || ""}</Typography.Paragraph>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={(): void => {
|
||||
auth.signOut().catch((error) => {
|
||||
console.error("Sign out error:", error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("navigation.signout")}
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Welcome;
|
||||
Reference in New Issue
Block a user