F: Add zod validation for query params
This commit is contained in:
parent
d139c07b7a
commit
d81e015be1
|
@ -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
|
||||
|
|
2
rsconcept/frontend/package-lock.json
generated
2
rsconcept/frontend/package-lock.json
generated
|
@ -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"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user