Added settings menu with working currency editing
This commit is contained in:
@@ -41,7 +41,8 @@
|
|||||||
"deleteError": "Error when deleting {{resource}} (status code: {{statusCode}})",
|
"deleteError": "Error when deleting {{resource}} (status code: {{statusCode}})",
|
||||||
"editSuccess": "Successfully edited {{resource}}",
|
"editSuccess": "Successfully edited {{resource}}",
|
||||||
"editError": "Error when editing {{resource}} (status code: {{statusCode}})",
|
"editError": "Error when editing {{resource}} (status code: {{statusCode}})",
|
||||||
"importProgress": "Importing: {{processed}}/{{total}}"
|
"importProgress": "Importing: {{processed}}/{{total}}",
|
||||||
|
"saveSuccessful": "Save successful!"
|
||||||
},
|
},
|
||||||
"kofi": "Tip me on Ko-fi",
|
"kofi": "Tip me on Ko-fi",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
@@ -252,6 +253,18 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"actions": "Actions"
|
"actions": "Actions"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"header": "Settings",
|
||||||
|
"generic": {
|
||||||
|
"tab": "Generic",
|
||||||
|
"currency": {
|
||||||
|
"label": "Currency"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra_fields": {
|
||||||
|
"tab": "Extra Fields"
|
||||||
|
}
|
||||||
|
},
|
||||||
"documentTitle": {
|
"documentTitle": {
|
||||||
"default": "Spoolman",
|
"default": "Spoolman",
|
||||||
"suffix": " | Spoolman",
|
"suffix": " | Spoolman",
|
||||||
|
|||||||
@@ -41,7 +41,8 @@
|
|||||||
"deleteError": "Det gick inte att ta bort {{resource}} (statuskod: {{statusCode}})",
|
"deleteError": "Det gick inte att ta bort {{resource}} (statuskod: {{statusCode}})",
|
||||||
"editSuccess": "Lyckades ändra {{resource}}",
|
"editSuccess": "Lyckades ändra {{resource}}",
|
||||||
"editError": "Det gick inte att ändra {{resource}} (statuskod: {{statusCode}})",
|
"editError": "Det gick inte att ändra {{resource}} (statuskod: {{statusCode}})",
|
||||||
"importProgress": "Importerar: {{processed}}/{{total}}"
|
"importProgress": "Importerar: {{processed}}/{{total}}",
|
||||||
|
"saveSuccessful": "Sparning lyckades!"
|
||||||
},
|
},
|
||||||
"loading": "Laddar",
|
"loading": "Laddar",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
@@ -240,6 +241,18 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"actions": "Åtgärder"
|
"actions": "Åtgärder"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"header": "Inställningar",
|
||||||
|
"generic": {
|
||||||
|
"tab": "Allmänt",
|
||||||
|
"currency": {
|
||||||
|
"label": "Valuta"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra_fields": {
|
||||||
|
"tab": "Extra Fält"
|
||||||
|
}
|
||||||
|
},
|
||||||
"documentTitle": {
|
"documentTitle": {
|
||||||
"default": "Spoolman",
|
"default": "Spoolman",
|
||||||
"suffix": " | Spoolman",
|
"suffix": " | Spoolman",
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ import dataProvider from "./components/dataProvider";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
|
||||||
import { ColorModeContextProvider } from "./contexts/color-mode";
|
import { ColorModeContextProvider } from "./contexts/color-mode";
|
||||||
import { FileOutlined, HighlightOutlined, HomeOutlined, QuestionOutlined, UserOutlined } from "@ant-design/icons";
|
import {
|
||||||
|
FileOutlined,
|
||||||
|
HighlightOutlined,
|
||||||
|
HomeOutlined,
|
||||||
|
QuestionOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import { ConfigProvider } from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Locale } from "antd/es/locale";
|
import { Locale } from "antd/es/locale";
|
||||||
@@ -139,6 +146,14 @@ function App() {
|
|||||||
icon: <UserOutlined />,
|
icon: <UserOutlined />,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "settings",
|
||||||
|
list: "/settings",
|
||||||
|
meta: {
|
||||||
|
canDelete: false,
|
||||||
|
icon: <ToolOutlined />,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "help",
|
name: "help",
|
||||||
list: "/help",
|
list: "/help",
|
||||||
@@ -202,6 +217,7 @@ function App() {
|
|||||||
<Route path="edit/:id" element={<LoadableResourcePage resource="vendors" page="edit" />} />
|
<Route path="edit/:id" element={<LoadableResourcePage resource="vendors" page="edit" />} />
|
||||||
<Route path="show/:id" element={<LoadableResourcePage resource="vendors" page="show" />} />
|
<Route path="show/:id" element={<LoadableResourcePage resource="vendors" page="show" />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/settings" element={<LoadablePage name="settings" />} />
|
||||||
<Route path="/help" element={<LoadablePage name="help" />} />
|
<Route path="/help" element={<LoadablePage name="help" />} />
|
||||||
<Route path="*" element={<ErrorComponent />} />
|
<Route path="*" element={<ErrorComponent />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
5
client/src/pages/settings/ExtraFieldsSettings.tsx
Normal file
5
client/src/pages/settings/ExtraFieldsSettings.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function ExtraFieldsSettings() {
|
||||||
|
return <>Extra Fields Settings Content</>;
|
||||||
|
}
|
||||||
79
client/src/pages/settings/GenericSettings.tsx
Normal file
79
client/src/pages/settings/GenericSettings.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslate } from "@refinedev/core";
|
||||||
|
import { Button, Form, Input, message } from "antd";
|
||||||
|
import { useGetSettings, useSetSetting } from "./query";
|
||||||
|
|
||||||
|
export function GenericSettings() {
|
||||||
|
const settings = useGetSettings();
|
||||||
|
const setSetting = useSetSetting();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
|
// Set initial form values
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (settings.data) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
currency: JSON.parse(settings.data.currency.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [settings.data, form]);
|
||||||
|
|
||||||
|
// Popup message if setSetting is successful
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (setSetting.isSuccess) {
|
||||||
|
messageApi.success(t("notifications.saveSuccessful"));
|
||||||
|
}
|
||||||
|
}, [setSetting.isSuccess, messageApi, t]);
|
||||||
|
|
||||||
|
// Handle form submit
|
||||||
|
const onFinish = (values: { currency: string }) => {
|
||||||
|
// Check if the currency has changed
|
||||||
|
if (settings.data?.currency.value !== JSON.stringify(values.currency)) {
|
||||||
|
setSetting.mutate({
|
||||||
|
key: "currency",
|
||||||
|
value: JSON.stringify(values.currency),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
labelCol={{ span: 8 }}
|
||||||
|
wrapperCol={{ span: 16 }}
|
||||||
|
initialValues={{
|
||||||
|
currency: settings.data?.currency.value,
|
||||||
|
}}
|
||||||
|
onFinish={onFinish}
|
||||||
|
style={{
|
||||||
|
maxWidth: "600px",
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t("settings.generic.currency.label")}
|
||||||
|
name="currency"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^[A-Z]{3}$/,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
|
||||||
|
<Button type="primary" htmlType="submit" loading={settings.isFetching || setSetting.isLoading}>
|
||||||
|
{t("buttons.save")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
{contextHolder}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
client/src/pages/settings/index.tsx
Normal file
64
client/src/pages/settings/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
import { Content } from "antd/es/layout/layout";
|
||||||
|
import { Menu, theme } from "antd";
|
||||||
|
import { SolutionOutlined, ToolOutlined } from "@ant-design/icons";
|
||||||
|
import { useSavedState } from "../../utils/saveload";
|
||||||
|
import { GenericSettings } from "./GenericSettings";
|
||||||
|
import { ExtraFieldsSettings } from "./ExtraFieldsSettings";
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
|
||||||
|
const { useToken } = theme;
|
||||||
|
|
||||||
|
export const Settings: React.FC<IResourceComponentsProps> = () => {
|
||||||
|
const { token } = useToken();
|
||||||
|
const t = useTranslate();
|
||||||
|
const [current, setCurrent] = useSavedState("settings-tab", "generic");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
color: token.colorText,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("settings.header")}
|
||||||
|
</h1>
|
||||||
|
<Content
|
||||||
|
style={{
|
||||||
|
padding: "1em",
|
||||||
|
minHeight: 280,
|
||||||
|
margin: "0 auto",
|
||||||
|
backgroundColor: token.colorBgContainer,
|
||||||
|
borderRadius: token.borderRadiusLG,
|
||||||
|
color: token.colorText,
|
||||||
|
fontFamily: token.fontFamily,
|
||||||
|
fontSize: token.fontSizeLG,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
mode="horizontal"
|
||||||
|
selectedKeys={[current]}
|
||||||
|
onClick={({ key }) => setCurrent(key)}
|
||||||
|
items={[
|
||||||
|
{ key: "generic", label: t("settings.generic.tab"), icon: <ToolOutlined /> },
|
||||||
|
{ key: "extra", label: t("settings.extra_fields.tab"), icon: <SolutionOutlined /> },
|
||||||
|
]}
|
||||||
|
style={{
|
||||||
|
marginBottom: "3em",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<main>
|
||||||
|
{current === "generic" && <GenericSettings />}
|
||||||
|
{current === "extra" && <ExtraFieldsSettings />}
|
||||||
|
</main>
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Settings;
|
||||||
55
client/src/pages/settings/query.ts
Normal file
55
client/src/pages/settings/query.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
interface SettingResponseValue {
|
||||||
|
value: string;
|
||||||
|
is_set: boolean;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsResponse {
|
||||||
|
[key: string]: SettingResponseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetSettings() {
|
||||||
|
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||||
|
return useQuery<SettingsResponse>({
|
||||||
|
queryKey: ["settings"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`${apiEndpoint}/setting`);
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetSetting(key: string) {
|
||||||
|
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||||
|
return useQuery<SettingResponseValue>({
|
||||||
|
queryKey: ["settings", key],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`${apiEndpoint}/setting/${key}`);
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSetSetting() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||||
|
return useMutation<SettingResponseValue, unknown, { key: string; value: unknown }>({
|
||||||
|
mutationFn: async ({ key, value }) => {
|
||||||
|
const response = await fetch(`${apiEndpoint}/setting/${key}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(value),
|
||||||
|
});
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
onSuccess: (_data, { key }) => {
|
||||||
|
// Invalidate and refetch
|
||||||
|
queryClient.invalidateQueries(["settings", key]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user