mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
F: Add zod validation for query params
This commit is contained in:
parent
55d7b919c8
commit
2b08a5a966
|
@ -45,6 +45,7 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- qrcode.react
|
- qrcode.react
|
||||||
- html-to-image
|
- html-to-image
|
||||||
- zustand
|
- zustand
|
||||||
|
- zod
|
||||||
- @tanstack/react-table
|
- @tanstack/react-table
|
||||||
- @tanstack/react-query
|
- @tanstack/react-query
|
||||||
- @tanstack/react-query-devtools
|
- @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",
|
"react-zoom-pan-pinch": "^3.6.1",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
|
"zod": "^3.24.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -10932,7 +10933,6 @@
|
||||||
"version": "3.24.1",
|
"version": "3.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
"react-zoom-pan-pinch": "^3.6.1",
|
"react-zoom-pan-pinch": "^3.6.1",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
|
"zod": "^3.24.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -24,18 +24,6 @@ export const routes = {
|
||||||
database_schema: 'database-schema'
|
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.
|
* Internal navigation URLs.
|
||||||
*/
|
*/
|
||||||
|
@ -58,12 +46,24 @@ export const urls = {
|
||||||
schema: (id: number | string, version?: number | string) =>
|
schema: (id: number | string, version?: number | string) =>
|
||||||
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
|
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
|
||||||
oss: (id: number | string, tab?: number) => `/oss/${id}` + (tab !== undefined ? `?tab=${tab}` : ''),
|
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 versionStr = version !== undefined ? `v=${version}&` : '';
|
||||||
const activeStr = active !== undefined ? `&active=${active}` : '';
|
const activeStr = active !== undefined ? `&active=${active}` : '';
|
||||||
return `/rsforms/${id}?${versionStr}tab=${tab}${activeStr}`;
|
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}`;
|
return `/oss/${id}?tab=${tab}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,12 +19,11 @@ import { resources } from '@/utils/constants';
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
const userQuery = query.get('username');
|
|
||||||
|
|
||||||
const { isAnonymous } = useAuthSuspense();
|
const { isAnonymous } = useAuthSuspense();
|
||||||
const { login, isPending, error, reset } = useLogin();
|
const { login, isPending, error, reset } = useLogin();
|
||||||
|
|
||||||
const [username, setUsername] = useState(userQuery || '');
|
const [username, setUsername] = useState(query.get('username') ?? '');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import ViewTopic from './ViewTopic';
|
||||||
export function ManualsPage() {
|
export function ManualsPage() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
const activeTopic = (query.get('topic') || HelpTopic.MAIN) as HelpTopic;
|
const activeTopic = (query.get('topic') ?? HelpTopic.MAIN) as HelpTopic;
|
||||||
|
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,53 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { ErrorData } from '@/components/info/InfoError';
|
import { ErrorData } from '@/components/info/InfoError';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
|
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
import { OssEditState } from './OssEditContext';
|
import { OssEditState, OssTabID } from './OssEditContext';
|
||||||
import OssTabs from './OssTabs';
|
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() {
|
export function OssPage() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const params = useParams();
|
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);
|
useBlockNavigation(isModified);
|
||||||
|
|
||||||
if (!itemID) {
|
useEffect(() => setIsModified(false), [setIsModified]);
|
||||||
|
|
||||||
|
if (!urlData.id) {
|
||||||
router.replace(urls.page404);
|
router.replace(urls.page404);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ProcessError}>
|
<ErrorBoundary FallbackComponent={ProcessError}>
|
||||||
<OssEditState itemID={itemID}>
|
<OssEditState itemID={urlData.id}>
|
||||||
<OssTabs />
|
<OssTabs activeTab={urlData.tab} />
|
||||||
</OssEditState>
|
</OssEditState>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,28 +7,23 @@ import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import TabLabel from '@/components/ui/TabLabel';
|
import TabLabel from '@/components/ui/TabLabel';
|
||||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
|
||||||
|
|
||||||
import EditorRSForm from './EditorOssCard';
|
import EditorRSForm from './EditorOssCard';
|
||||||
import EditorTermGraph from './EditorOssGraph';
|
import EditorTermGraph from './EditorOssGraph';
|
||||||
import MenuOssTabs from './MenuOssTabs';
|
import MenuOssTabs from './MenuOssTabs';
|
||||||
import { OssTabID, useOssEdit } from './OssEditContext';
|
import { OssTabID, useOssEdit } from './OssEditContext';
|
||||||
|
|
||||||
function OssTabs() {
|
interface OssTabsProps {
|
||||||
const router = useConceptNavigation();
|
activeTab: OssTabID;
|
||||||
const query = useQueryStrings();
|
}
|
||||||
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
|
|
||||||
|
|
||||||
|
function OssTabs({ activeTab }: OssTabsProps) {
|
||||||
|
const router = useConceptNavigation();
|
||||||
const { schema, navigateTab } = useOssEdit();
|
const { schema, navigateTab } = useOssEdit();
|
||||||
|
|
||||||
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
||||||
|
|
||||||
const { setIsModified } = useModificationStore();
|
|
||||||
|
|
||||||
useEffect(() => setIsModified(false), [setIsModified]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const oldTitle = document.title;
|
const oldTitle = document.title;
|
||||||
document.title = schema.title;
|
document.title = schema.title;
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
@ -16,29 +18,48 @@ import { useModificationStore } from '@/stores/modification';
|
||||||
import { RSEditState, RSTabID } from './RSEditContext';
|
import { RSEditState, RSTabID } from './RSEditContext';
|
||||||
import RSTabs from './RSTabs';
|
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() {
|
export function RSFormPage() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const query = useQueryStrings();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const itemID = params.id ? Number(params.id) : undefined;
|
const query = useQueryStrings();
|
||||||
const version = query.get('v') ? Number(query.get('v')) : undefined;
|
|
||||||
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
|
|
||||||
|
|
||||||
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);
|
useBlockNavigation(isModified);
|
||||||
|
|
||||||
if (!itemID) {
|
useEffect(() => setIsModified(false), [setIsModified]);
|
||||||
|
|
||||||
|
if (!urlData.id) {
|
||||||
router.replace(urls.page404);
|
router.replace(urls.page404);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
FallbackComponent={({ error }) => (
|
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}>
|
<RSEditState itemID={urlData.id} activeVersion={urlData.version} activeTab={urlData.tab}>
|
||||||
<RSTabs />
|
<RSTabs activeID={urlData.activeID} activeTab={urlData.tab} />
|
||||||
</RSEditState>
|
</RSEditState>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import TabLabel from '@/components/ui/TabLabel';
|
import TabLabel from '@/components/ui/TabLabel';
|
||||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
import { ConstituentaID } from '@/models/rsform';
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { labelVersion } from '@/utils/labels';
|
import { labelVersion } from '@/utils/labels';
|
||||||
|
@ -19,18 +19,18 @@ import EditorTermGraph from './EditorTermGraph';
|
||||||
import MenuRSTabs from './MenuRSTabs';
|
import MenuRSTabs from './MenuRSTabs';
|
||||||
import { RSTabID, useRSEdit } from './RSEditContext';
|
import { RSTabID, useRSEdit } from './RSEditContext';
|
||||||
|
|
||||||
function RSTabs() {
|
interface RSTabsProps {
|
||||||
const query = useQueryStrings();
|
activeID?: ConstituentaID;
|
||||||
|
activeTab: RSTabID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RSTabs({ activeID, activeTab }: RSTabsProps) {
|
||||||
const router = useConceptNavigation();
|
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 hideFooter = useAppLayoutStore(state => state.hideFooter);
|
||||||
const { setIsModified } = useModificationStore();
|
const { setIsModified } = useModificationStore();
|
||||||
const { schema, selected, setSelected, navigateRSForm } = useRSEdit();
|
const { schema, selected, setSelected, navigateRSForm } = useRSEdit();
|
||||||
|
|
||||||
useEffect(() => setIsModified(false), [setIsModified]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const oldTitle = document.title;
|
const oldTitle = document.title;
|
||||||
document.title = schema.title;
|
document.title = schema.title;
|
||||||
|
@ -43,15 +43,14 @@ function RSTabs() {
|
||||||
hideFooter(activeTab !== RSTabID.CARD);
|
hideFooter(activeTab !== RSTabID.CARD);
|
||||||
setIsModified(false);
|
setIsModified(false);
|
||||||
if (activeTab === RSTabID.CST_EDIT) {
|
if (activeTab === RSTabID.CST_EDIT) {
|
||||||
const cstID = Number(cstQuery);
|
if (activeID && schema.cstByID.has(activeID)) {
|
||||||
if (cstID && schema.cstByID.has(cstID)) {
|
setSelected([activeID]);
|
||||||
setSelected([cstID]);
|
|
||||||
} else {
|
} else {
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return () => hideFooter(false);
|
return () => hideFooter(false);
|
||||||
}, [activeTab, cstQuery, setSelected, schema, hideFooter, setIsModified]);
|
}, [activeTab, activeID, setSelected, schema, hideFooter, setIsModified]);
|
||||||
|
|
||||||
function onSelectTab(index: number, last: number, event: Event) {
|
function onSelectTab(index: number, last: number, event: Event) {
|
||||||
if (last === index) {
|
if (last === index) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user