F: Add zod validation for query params

This commit is contained in:
Ivan 2025-01-30 19:23:15 +03:00
parent 55d7b919c8
commit 2b08a5a966
10 changed files with 88 additions and 54 deletions

View File

@ -45,6 +45,7 @@ This readme file is used mostly to document project dependencies and conventions
- qrcode.react
- html-to-image
- zustand
- zod
- @tanstack/react-table
- @tanstack/react-query
- @tanstack/react-query-devtools

View File

@ -33,6 +33,7 @@
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4",
"use-debounce": "^10.0.4",
"zod": "^3.24.1",
"zustand": "^5.0.3"
},
"devDependencies": {
@ -10932,7 +10933,6 @@
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@ -37,6 +37,7 @@
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4",
"use-debounce": "^10.0.4",
"zod": "^3.24.1",
"zustand": "^5.0.3"
},
"devDependencies": {

View File

@ -24,18 +24,6 @@ export const routes = {
database_schema: 'database-schema'
};
interface SchemaProps {
id: number | string;
tab: number;
version?: number | string;
active?: number | string;
}
interface OssProps {
id: number | string;
tab: number;
}
/**
* Internal navigation URLs.
*/
@ -58,12 +46,24 @@ export const urls = {
schema: (id: number | string, version?: number | string) =>
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
oss: (id: number | string, tab?: number) => `/oss/${id}` + (tab !== undefined ? `?tab=${tab}` : ''),
schema_props: ({ id, tab, version, active }: SchemaProps) => {
schema_props: ({
id,
tab,
version,
active
}: {
id: number | string;
tab: number;
version?: number | string;
active?: number | string;
}) => {
const versionStr = version !== undefined ? `v=${version}&` : '';
const activeStr = active !== undefined ? `&active=${active}` : '';
return `/rsforms/${id}?${versionStr}tab=${tab}${activeStr}`;
},
oss_props: ({ id, tab }: OssProps) => {
oss_props: ({ id, tab }: { id: number | string; tab: number }) => {
return `/oss/${id}?tab=${tab}`;
}
};

View File

@ -19,12 +19,11 @@ import { resources } from '@/utils/constants';
function LoginPage() {
const router = useConceptNavigation();
const query = useQueryStrings();
const userQuery = query.get('username');
const { isAnonymous } = useAuthSuspense();
const { login, isPending, error, reset } = useLogin();
const [username, setUsername] = useState(userQuery || '');
const [username, setUsername] = useState(query.get('username') ?? '');
const [password, setPassword] = useState('');
useEffect(() => {

View File

@ -13,7 +13,7 @@ import ViewTopic from './ViewTopic';
export function ManualsPage() {
const router = useConceptNavigation();
const query = useQueryStrings();
const activeTopic = (query.get('topic') || HelpTopic.MAIN) as HelpTopic;
const activeTopic = (query.get('topic') ?? HelpTopic.MAIN) as HelpTopic;
const mainHeight = useMainHeight();

View File

@ -1,35 +1,53 @@
'use client';
import axios from 'axios';
import { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router';
import { z } from 'zod';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { ErrorData } from '@/components/info/InfoError';
import TextURL from '@/components/ui/TextURL';
import useQueryStrings from '@/hooks/useQueryStrings';
import { useModificationStore } from '@/stores/modification';
import { OssEditState } from './OssEditContext';
import { OssEditState, OssTabID } from './OssEditContext';
import OssTabs from './OssTabs';
const paramsSchema = z.object({
id: z
.string()
.nullish()
.transform(v => (v ? Number(v) : undefined)),
tab: z.preprocess(v => (v ? Number(v) : undefined), z.nativeEnum(OssTabID).default(OssTabID.CARD))
});
export function OssPage() {
const router = useConceptNavigation();
const params = useParams();
const itemID = params.id ? Number(params.id) : undefined;
const query = useQueryStrings();
const { isModified } = useModificationStore();
const urlData = paramsSchema.parse({
id: params.id,
tab: query.get('tab')
});
const { isModified, setIsModified } = useModificationStore();
useBlockNavigation(isModified);
if (!itemID) {
useEffect(() => setIsModified(false), [setIsModified]);
if (!urlData.id) {
router.replace(urls.page404);
return null;
}
return (
<ErrorBoundary FallbackComponent={ProcessError}>
<OssEditState itemID={itemID}>
<OssTabs />
<OssEditState itemID={urlData.id}>
<OssTabs activeTab={urlData.tab} />
</OssEditState>
</ErrorBoundary>
);

View File

@ -7,28 +7,23 @@ import { TabList, TabPanel, Tabs } from 'react-tabs';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import useQueryStrings from '@/hooks/useQueryStrings';
import { useAppLayoutStore } from '@/stores/appLayout';
import { useModificationStore } from '@/stores/modification';
import EditorRSForm from './EditorOssCard';
import EditorTermGraph from './EditorOssGraph';
import MenuOssTabs from './MenuOssTabs';
import { OssTabID, useOssEdit } from './OssEditContext';
function OssTabs() {
const router = useConceptNavigation();
const query = useQueryStrings();
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
interface OssTabsProps {
activeTab: OssTabID;
}
function OssTabs({ activeTab }: OssTabsProps) {
const router = useConceptNavigation();
const { schema, navigateTab } = useOssEdit();
const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { setIsModified } = useModificationStore();
useEffect(() => setIsModified(false), [setIsModified]);
useEffect(() => {
const oldTitle = document.title;
document.title = schema.title;

View File

@ -1,8 +1,10 @@
'use client';
import axios from 'axios';
import { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router';
import { z } from 'zod';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
@ -16,29 +18,48 @@ import { useModificationStore } from '@/stores/modification';
import { RSEditState, RSTabID } from './RSEditContext';
import RSTabs from './RSTabs';
const paramsSchema = z.object({
id: z
.string()
.nullish()
.transform(v => (v ? Number(v) : undefined)),
version: z
.string()
.nullish()
.transform(v => (v ? Number(v) : undefined)),
tab: z.preprocess(v => (v ? Number(v) : undefined), z.nativeEnum(RSTabID).default(RSTabID.CARD)),
activeID: z.preprocess(v => (v ? Number(v) : undefined), z.number().optional())
});
export function RSFormPage() {
const router = useConceptNavigation();
const query = useQueryStrings();
const params = useParams();
const itemID = params.id ? Number(params.id) : undefined;
const version = query.get('v') ? Number(query.get('v')) : undefined;
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
const query = useQueryStrings();
const { isModified } = useModificationStore();
const urlData = paramsSchema.parse({
id: params.id,
version: query.get('v'),
tab: query.get('tab'),
activeID: query.get('active')
});
const { isModified, setIsModified } = useModificationStore();
useBlockNavigation(isModified);
if (!itemID) {
useEffect(() => setIsModified(false), [setIsModified]);
if (!urlData.id) {
router.replace(urls.page404);
return null;
}
return (
<ErrorBoundary
FallbackComponent={({ error }) => (
<ProcessError error={error as ErrorData} isArchive={!!version} itemID={itemID} />
<ProcessError error={error as ErrorData} isArchive={!!urlData.version} itemID={urlData.id} />
)}
>
<RSEditState itemID={itemID} activeVersion={version} activeTab={activeTab}>
<RSTabs />
<RSEditState itemID={urlData.id} activeVersion={urlData.version} activeTab={urlData.tab}>
<RSTabs activeID={urlData.activeID} activeTab={urlData.tab} />
</RSEditState>
</ErrorBoundary>
);

View File

@ -7,7 +7,7 @@ import { TabList, TabPanel, Tabs } from 'react-tabs';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import useQueryStrings from '@/hooks/useQueryStrings';
import { ConstituentaID } from '@/models/rsform';
import { useAppLayoutStore } from '@/stores/appLayout';
import { useModificationStore } from '@/stores/modification';
import { labelVersion } from '@/utils/labels';
@ -19,18 +19,18 @@ import EditorTermGraph from './EditorTermGraph';
import MenuRSTabs from './MenuRSTabs';
import { RSTabID, useRSEdit } from './RSEditContext';
function RSTabs() {
const query = useQueryStrings();
interface RSTabsProps {
activeID?: ConstituentaID;
activeTab: RSTabID;
}
function RSTabs({ activeID, activeTab }: RSTabsProps) {
const router = useConceptNavigation();
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
const cstQuery = query.get('active');
const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { setIsModified } = useModificationStore();
const { schema, selected, setSelected, navigateRSForm } = useRSEdit();
useEffect(() => setIsModified(false), [setIsModified]);
useEffect(() => {
const oldTitle = document.title;
document.title = schema.title;
@ -43,15 +43,14 @@ function RSTabs() {
hideFooter(activeTab !== RSTabID.CARD);
setIsModified(false);
if (activeTab === RSTabID.CST_EDIT) {
const cstID = Number(cstQuery);
if (cstID && schema.cstByID.has(cstID)) {
setSelected([cstID]);
if (activeID && schema.cstByID.has(activeID)) {
setSelected([activeID]);
} else {
setSelected([]);
}
}
return () => hideFooter(false);
}, [activeTab, cstQuery, setSelected, schema, hideFooter, setIsModified]);
}, [activeTab, activeID, setSelected, schema, hideFooter, setIsModified]);
function onSelectTab(index: number, last: number, event: Event) {
if (last === index) {