storing changes, starting history list
This commit is contained in:
@@ -33,8 +33,6 @@ export async function handleSystemSave(values: SystemValues) {
|
|||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
alert("System Settings Saved Successfully!");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,8 @@
|
|||||||
import React from "react";
|
|
||||||
import Card from "../../UI/Card";
|
import Card from "../../UI/Card";
|
||||||
import CardHeader from "../../UI/CardHeader";
|
import CardHeader from "../../UI/CardHeader";
|
||||||
import { sendBlobFileUpload } from "./Upload";
|
|
||||||
import SystemConfigFields from "./SystemConfigFields.tsx";
|
import SystemConfigFields from "./SystemConfigFields.tsx";
|
||||||
|
|
||||||
const SystemCard = () => {
|
const SystemCard = () => {
|
||||||
const [selectedFile, setSelectedFile] = React.useState<File | null>(null);
|
|
||||||
const [error, setError] = React.useState("");
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// (async () => {
|
|
||||||
// const result = await handleSystemRecall(); // returns { deviceName, sntpServer, sntpInterval, timeZone } | null
|
|
||||||
// if (result) {
|
|
||||||
// const {
|
|
||||||
// deviceName: dn,
|
|
||||||
// sntpServer: ss,
|
|
||||||
// sntpInterval: si,
|
|
||||||
// timeZone: tz,
|
|
||||||
// } = result;
|
|
||||||
|
|
||||||
// setDeviceName(dn ?? "");
|
|
||||||
// setSntpServer(ss ?? "");
|
|
||||||
// setSntpInterval(Number.isFinite(si) ? si : 60);
|
|
||||||
// setTimeZone(tz ?? "UTC (UTC-00)");
|
|
||||||
// }
|
|
||||||
// })();
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault(); // prevent full page reload
|
|
||||||
if (!selectedFile) {
|
|
||||||
setError("Please select a file before uploading.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
const result = await sendBlobFileUpload(selectedFile, {
|
|
||||||
timeoutMs: 30000,
|
|
||||||
fieldName: "upload",
|
|
||||||
});
|
|
||||||
|
|
||||||
// The helper returns a string (either success body or formatted error)
|
|
||||||
// You can decide how to distinguish. Here, we show it optimistically and let the text speak.
|
|
||||||
if (
|
|
||||||
result.startsWith("Server returned") ||
|
|
||||||
result.startsWith("Timeout") ||
|
|
||||||
result.startsWith("HTTP error") ||
|
|
||||||
result.startsWith("Unexpected error")
|
|
||||||
) {
|
|
||||||
setError(result);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader title={"System Config"} />
|
<CardHeader title={"System Config"} />
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Formik, Field, Form } from "formik";
|
import { Formik, Field, Form } from "formik";
|
||||||
import FormGroup from "../components/FormGroup";
|
import FormGroup from "../components/FormGroup";
|
||||||
import { handleSoftReboot, handleHardReboot } from "./Reboots";
|
import { handleSoftReboot, handleHardReboot } from "./Reboots";
|
||||||
import { handleSystemSave } from "./SettingSaveRecall";
|
|
||||||
import { timezones } from "./timezones";
|
import { timezones } from "./timezones";
|
||||||
import SystemFileUpload from "./SystemFileUpload";
|
import SystemFileUpload from "./SystemFileUpload";
|
||||||
import type { SystemValues, SystemValuesErrors } from "../../../types/types";
|
import type { SystemValues, SystemValuesErrors } from "../../../types/types";
|
||||||
|
import { useSystemConfig } from "../../../hooks/useSystemConfig";
|
||||||
|
|
||||||
const SystemConfigFields = () => {
|
const SystemConfigFields = () => {
|
||||||
|
const { saveSystemSettings, getSystemSettingsData } = useSystemConfig();
|
||||||
|
console.log(getSystemSettingsData);
|
||||||
const initialvalues: SystemValues = {
|
const initialvalues: SystemValues = {
|
||||||
deviceName: "",
|
deviceName: "",
|
||||||
timeZone: "",
|
timeZone: "",
|
||||||
@@ -15,7 +17,7 @@ const SystemConfigFields = () => {
|
|||||||
softwareUpdate: null,
|
softwareUpdate: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (values: SystemValues) => handleSystemSave(values);
|
const handleSubmit = (values: SystemValues) => saveSystemSettings(values);
|
||||||
const validateValues = (values: SystemValues) => {
|
const validateValues = (values: SystemValues) => {
|
||||||
const errors: SystemValuesErrors = {};
|
const errors: SystemValuesErrors = {};
|
||||||
const interval = Number(values.sntpInterval);
|
const interval = Number(values.sntpInterval);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useFormikContext } from "formik";
|
import { useFormikContext } from "formik";
|
||||||
|
|
||||||
import FormGroup from "../components/FormGroup";
|
import FormGroup from "../components/FormGroup";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useSystemConfig } from "../../../hooks/useSystemConfig";
|
||||||
|
|
||||||
type SystemFileUploadProps = {
|
type SystemFileUploadProps = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -10,7 +10,16 @@ type SystemFileUploadProps = {
|
|||||||
|
|
||||||
const SystemFileUpload = ({ name, selectedFile }: SystemFileUploadProps) => {
|
const SystemFileUpload = ({ name, selectedFile }: SystemFileUploadProps) => {
|
||||||
const { setFieldValue } = useFormikContext();
|
const { setFieldValue } = useFormikContext();
|
||||||
const handleFileUploadClick = () => console.log(selectedFile);
|
const { uploadSettings } = useSystemConfig();
|
||||||
|
|
||||||
|
const handleFileUploadClick = () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
uploadSettings(selectedFile, {
|
||||||
|
timeoutMs: 30000,
|
||||||
|
fieldName: "upload",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-8 w-full">
|
<div className="py-8 w-full">
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function sendBlobFileUpload(
|
|||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append(fieldName, file, fileName);
|
form.append(fieldName, file, fileName);
|
||||||
|
|
||||||
const resp = await fetch('http://192.168.75.11/upload/software-update/2', {
|
const resp = await fetch("http://192.168.75.11/upload/hotlist-upload/2", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: form,
|
body: form,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
@@ -24,6 +24,9 @@ export async function sendBlobFileUpload(
|
|||||||
const bodyText = await resp.text();
|
const bodyText = await resp.text();
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
|
// throw new Error(
|
||||||
|
// `Server returned ${resp.status}: ${resp.statusText}. Details: ${bodyText}`
|
||||||
|
// );
|
||||||
return `Server returned ${resp.status}: ${resp.statusText}. Details: ${bodyText}`;
|
return `Server returned ${resp.status}: ${resp.statusText}. Details: ${bodyText}`;
|
||||||
}
|
}
|
||||||
return bodyText;
|
return bodyText;
|
||||||
@@ -35,7 +38,9 @@ export async function sendBlobFileUpload(
|
|||||||
if (err instanceof TypeError) {
|
if (err instanceof TypeError) {
|
||||||
return `HTTP error uploading to /upload/software-update/2: ${err.message}`;
|
return `HTTP error uploading to /upload/software-update/2: ${err.message}`;
|
||||||
}
|
}
|
||||||
return `Unexpected error uploading to /upload/software-update/2: ${err?.message ?? String(err)} ${err?.cause ?? ""}`;
|
return `Unexpected error uploading to /upload/software-update/2: ${
|
||||||
|
err?.message ?? String(err)
|
||||||
|
} ${err?.cause ?? ""}`;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const SightingModal = ({
|
|||||||
sighting,
|
sighting,
|
||||||
}: SightingModalProps) => {
|
}: SightingModalProps) => {
|
||||||
const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY";
|
const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY";
|
||||||
|
console.log(sighting);
|
||||||
return (
|
return (
|
||||||
<ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}>
|
<ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,18 +17,21 @@ function useNow(tickMs = 1000) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SightingHistoryProps = {
|
type SightingHistoryProps = {
|
||||||
baseUrl: string;
|
baseUrl?: string;
|
||||||
entries?: number; // number of rows to show
|
entries?: number; // number of rows to show
|
||||||
pollMs?: number; // poll frequency
|
pollMs?: number; // poll frequency
|
||||||
autoSelectLatest?: boolean;
|
autoSelectLatest?: boolean;
|
||||||
|
title: string;
|
||||||
|
className: React.HTMLAttributes<HTMLDivElement> | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SightingHistoryWidgetProps = React.HTMLAttributes<HTMLDivElement>;
|
// /type SightingHistoryWidgetProps = React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export default function SightingHistoryWidget({
|
export default function SightingHistoryWidget({
|
||||||
className,
|
className,
|
||||||
}: SightingHistoryWidgetProps) {
|
title,
|
||||||
|
}: SightingHistoryProps) {
|
||||||
useNow(1000);
|
useNow(1000);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -40,7 +43,7 @@ export default function SightingHistoryWidget({
|
|||||||
} = useSightingFeedContext();
|
} = useSightingFeedContext();
|
||||||
|
|
||||||
const onRowClick = useCallback(
|
const onRowClick = useCallback(
|
||||||
(sighting: SightingType) => {
|
(sighting: SightingType | SightingWidgetType) => {
|
||||||
if (!sighting) return;
|
if (!sighting) return;
|
||||||
setSightingModalOpen(!isSightingModalOpen);
|
setSightingModalOpen(!isSightingModalOpen);
|
||||||
setSelectedSighting(sighting);
|
setSelectedSighting(sighting);
|
||||||
@@ -57,7 +60,7 @@ export default function SightingHistoryWidget({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className={clsx("overflow-y-auto h-100", className)}>
|
<Card className={clsx("overflow-y-auto h-100", className)}>
|
||||||
<CardHeader title="Front Camera Sightings" />
|
<CardHeader title={title} />
|
||||||
<div className="flex flex-col gap-3 ">
|
<div className="flex flex-col gap-3 ">
|
||||||
{/* Rows */}
|
{/* Rows */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -73,18 +76,31 @@ export default function SightingHistoryWidget({
|
|||||||
onClick={() => onRowClick(obj)}
|
onClick={() => onRowClick(obj)}
|
||||||
>
|
>
|
||||||
{/* Info bar */}
|
{/* Info bar */}
|
||||||
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded">
|
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded justify-between">
|
||||||
<div className="min-w-14">
|
<div className="flex items-center gap-3 text-xs">
|
||||||
CH: {obj ? obj.charHeight : "—"}
|
{" "}
|
||||||
|
<div className="min-w-14">
|
||||||
|
CH: {obj ? obj.charHeight : "—"}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-14">
|
||||||
|
Seen: {obj ? obj.seenCount : "—"}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-20">
|
||||||
|
{obj ? capitalize(obj.motion) : "—"}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-14 opacity-80">
|
||||||
|
{obj ? formatAge(obj.timeStampMillis) : "—"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-14">
|
|
||||||
Seen: {obj ? obj.seenCount : "—"}
|
<div className="min-w-14 opacity-80 ">
|
||||||
</div>
|
{isNPEDHit ? (
|
||||||
<div className="min-w-20">
|
<span className="text-red-500 font-semibold">
|
||||||
{obj ? capitalize(obj.motion) : "—"}
|
NPED HIT
|
||||||
</div>
|
</span>
|
||||||
<div className="min-w-14 opacity-80">
|
) : (
|
||||||
{obj ? formatAge(obj.timeStampMillis) : "—"}
|
""
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ type SightingFeedContextType = {
|
|||||||
mostRecent: SightingWidgetType | null;
|
mostRecent: SightingWidgetType | null;
|
||||||
side: string;
|
side: string;
|
||||||
selectedSighting: SightingType | null;
|
selectedSighting: SightingType | null;
|
||||||
setSelectedSighting: (sighting: SightingType | null) => void;
|
setSelectedSighting: (
|
||||||
|
sighting: SightingType | SightingWidgetType | null
|
||||||
|
) => void;
|
||||||
setSightingModalOpen: (isSightingModalOpen: boolean) => void;
|
setSightingModalOpen: (isSightingModalOpen: boolean) => void;
|
||||||
isSightingModalOpen: boolean;
|
isSightingModalOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
32
src/hooks/useSystemConfig.ts
Normal file
32
src/hooks/useSystemConfig.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { sendBlobFileUpload } from "../components/SettingForms/System/Upload";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
handleSystemSave,
|
||||||
|
handleSystemRecall,
|
||||||
|
} from "../components/SettingForms/System/SettingSaveRecall";
|
||||||
|
|
||||||
|
export const useSystemConfig = () => {
|
||||||
|
const uploadSettingsMutation = useMutation({
|
||||||
|
mutationKey: ["uploadSettings"],
|
||||||
|
mutationFn: sendBlobFileUpload,
|
||||||
|
onError: () => console.log("upload failed"),
|
||||||
|
onSuccess: (test) => console.log(test),
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveSystemSettings = useMutation({
|
||||||
|
mutationKey: ["systemSaveSettings"],
|
||||||
|
mutationFn: handleSystemSave,
|
||||||
|
onSuccess: () => toast("System Settings Saved Successfully!"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSystemSettings = useQuery({
|
||||||
|
queryKey: ["getSystemSettings"],
|
||||||
|
queryFn: handleSystemRecall,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
uploadSettings: uploadSettingsMutation.mutate,
|
||||||
|
saveSystemSettings: saveSystemSettings.mutate,
|
||||||
|
getSystemSettingsData: getSystemSettings.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard";
|
import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard";
|
||||||
import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraOverviewCard";
|
import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraOverviewCard";
|
||||||
import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget";
|
import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget";
|
||||||
|
|
||||||
import { SightingFeedProvider } from "../context/providers/SightingFeedProvider";
|
import { SightingFeedProvider } from "../context/providers/SightingFeedProvider";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
@@ -9,24 +8,30 @@ const Dashboard = () => {
|
|||||||
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-1 sm:px-2 lg:px-0 w-full">
|
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-1 sm:px-2 lg:px-0 w-full">
|
||||||
<SightingFeedProvider
|
<SightingFeedProvider
|
||||||
url={
|
url={
|
||||||
"http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
|
// "http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
|
||||||
// "http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef="
|
"http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef="
|
||||||
}
|
}
|
||||||
side="Front"
|
side="Front"
|
||||||
>
|
>
|
||||||
<FrontCameraOverviewCard className="order-1" />
|
<FrontCameraOverviewCard className="order-1" />
|
||||||
<SightingHistoryWidget className="order-5" />
|
<SightingHistoryWidget
|
||||||
|
className="order-3"
|
||||||
|
title="Front Camera Sightings"
|
||||||
|
/>
|
||||||
</SightingFeedProvider>
|
</SightingFeedProvider>
|
||||||
|
|
||||||
<SightingFeedProvider
|
<SightingFeedProvider
|
||||||
url={
|
url={
|
||||||
"http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
|
// "http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
|
||||||
// "http://192.168.75.11/SightingListRear/sightingSummary?mostRecentRef="
|
"http://192.168.75.11/SightingListRear/sightingSummary?mostRecentRef="
|
||||||
}
|
}
|
||||||
side="Rear"
|
side="Rear"
|
||||||
>
|
>
|
||||||
<RearCameraOverviewCard className="order-2" />
|
<RearCameraOverviewCard className="order-2" />
|
||||||
<SightingHistoryWidget className="order-4" />
|
<SightingHistoryWidget
|
||||||
|
className="order-4"
|
||||||
|
title="Rear Camera Sightings"
|
||||||
|
/>
|
||||||
</SightingFeedProvider>
|
</SightingFeedProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useNPEDAuth } from "../hooks/useNPEDAuth";
|
|||||||
|
|
||||||
const SystemSettings = () => {
|
const SystemSettings = () => {
|
||||||
const { user } = useNPEDAuth();
|
const { user } = useNPEDAuth();
|
||||||
console.log(user);
|
|
||||||
return (
|
return (
|
||||||
<div className="m-4">
|
<div className="m-4">
|
||||||
<Tabs selectedTabClassName="bg-gray-300 text-gray-900 font-semibold border-none">
|
<Tabs selectedTabClassName="bg-gray-300 text-gray-900 font-semibold border-none">
|
||||||
@@ -21,7 +21,7 @@ const SystemSettings = () => {
|
|||||||
<Tab>Integrations</Tab>
|
<Tab>Integrations</Tab>
|
||||||
<Tab>WiFi and Modem</Tab>
|
<Tab>WiFi and Modem</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<SystemCard />
|
<SystemCard />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,35 +71,36 @@ export type HotlistUploadType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SightingWidgetType = {
|
export type SightingWidgetType = {
|
||||||
|
debug: string;
|
||||||
|
SaFID: string;
|
||||||
ref: number; // unique, increasing
|
ref: number; // unique, increasing
|
||||||
idx?: number; // client-side slot index
|
idx?: number; // client-side slot index
|
||||||
vrm: string;
|
vrm: string;
|
||||||
vrmSecondary?: string; // empty string means missing
|
vrmSecondary: string; // empty string means missing
|
||||||
countryCode?: string;
|
countryCode: string;
|
||||||
timeStamp?: string; // formatted string
|
timeStamp: string; // formatted string
|
||||||
timeStampMillis: number; // epoch millis
|
timeStampMillis: number; // epoch millis
|
||||||
motion: string; // e.g., "AWAY" or "TOWARDS"
|
motion: string; // e.g., "AWAY" or "TOWARDS"
|
||||||
seenCount: number;
|
seenCount: string;
|
||||||
charHeight: string | number;
|
charHeight: string;
|
||||||
overviewUrl: string;
|
overviewUrl: string;
|
||||||
detailsUrl?: string;
|
detailsUrl: string;
|
||||||
make?: string;
|
make: string;
|
||||||
model?: string;
|
model: string;
|
||||||
color?: string;
|
color: string;
|
||||||
category?: string;
|
category: string;
|
||||||
plateSize?: string | number;
|
plateSize: string | number;
|
||||||
overviewSize?: string | number;
|
overviewSize: string | number;
|
||||||
locationName?: string;
|
locationName: string;
|
||||||
laneID?: string | number;
|
laneID: string | number;
|
||||||
radarSpeed?: string | number;
|
radarSpeed: string | number;
|
||||||
trackSpeed?: string | number;
|
trackSpeed: string | number;
|
||||||
srcCam?: 0 | 1;
|
srcCam: 0 | 1;
|
||||||
plateUrlInfrared?: string;
|
plateUrlInfrared: string;
|
||||||
plateUrlColour?: string;
|
plateUrlColour: string;
|
||||||
overviewPlateRect?: [number, number, number, number]; // [x,y,w,h] normalized 0..1
|
overviewPlateRect: [number, number, number, number]; // [x,y,w,h] normalized 0..1
|
||||||
plateTrack?: [number, number, number, number][];
|
plateTrack: [number, number, number, number][];
|
||||||
metadata?: Metadata;
|
metadata: Metadata;
|
||||||
// list of rects normalized 0..1
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VersionFieldType = {
|
export type VersionFieldType = {
|
||||||
|
|||||||
Reference in New Issue
Block a user