diff --git a/rsconcept/frontend/src/context/LibraryContext.tsx b/rsconcept/frontend/src/context/LibraryContext.tsx index 9c34a1c9..47b53d4b 100644 --- a/rsconcept/frontend/src/context/LibraryContext.tsx +++ b/rsconcept/frontend/src/context/LibraryContext.tsx @@ -95,7 +95,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => { return; } setError(undefined); - getRSFormDetails(String(templateID), { + getRSFormDetails(String(templateID), '', { showError: true, setLoading: setProcessing, onError: setError, diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index 9745213c..45c3cd3f 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -4,7 +4,7 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react import { type ErrorData } from '@/components/InfoError'; import useRSFormDetails from '@/hooks/useRSFormDetails'; -import { ILibraryItem } from '@/models/library'; +import { ILibraryItem, IVersionData } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library'; import { IConstituentaList, @@ -20,6 +20,7 @@ import { import { type DataCallback, deleteUnsubscribe, + deleteVersion, getTRSFile, patchConstituenta, patchDeleteConstituenta, @@ -29,7 +30,9 @@ import { patchResetAliases, patchSubstituteConstituenta, patchUploadTRS, + patchVersion, postClaimLibraryItem, + postCreateVersion, postNewConstituenta, postSubscribe } from '@/utils/backendAPI'; @@ -39,11 +42,13 @@ import { useLibrary } from './LibraryContext'; interface IRSFormContext { schema?: IRSForm; + schemaID: string; error: ErrorData; loading: boolean; processing: boolean; + isArchive: boolean; isOwned: boolean; isClaimable: boolean; isSubscribed: boolean; @@ -63,6 +68,10 @@ interface IRSFormContext { cstUpdate: (data: ICstUpdateData, callback?: DataCallback) => void; cstDelete: (data: IConstituentaList, callback?: () => void) => void; cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void; + + versionCreate: (data: IVersionData, callback?: () => void) => void; + versionUpdate: (target: number, data: IVersionData, callback?: () => void) => void; + versionDelete: (target: number, callback?: () => void) => void; } const RSFormContext = createContext(null); @@ -76,13 +85,24 @@ export const useRSForm = () => { interface RSFormStateProps { schemaID: string; + versionID?: string; children: React.ReactNode; } -export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { +export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps) => { const library = useLibrary(); const { user } = useAuth(); - const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID }); + const { + schema, // prettier: split lines + reload, + error, + setError, + setSchema, + loading + } = useRSFormDetails({ + target: schemaID, + version: versionID + }); const [processing, setProcessing] = useState(false); const [toggleTracking, setToggleTracking] = useState(false); @@ -91,6 +111,8 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { return user?.id === schema?.owner || false; }, [user, schema?.owner]); + const isArchive = useMemo(() => !!versionID, [versionID]); + const isClaimable = useMemo(() => { return (user?.id !== schema?.owner && schema?.is_common && !schema?.is_canonical) ?? false; }, [user, schema?.owner, schema?.is_common, schema?.is_canonical]); @@ -359,16 +381,79 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { [schemaID, setError, library, setSchema] ); + const versionCreate = useCallback( + (data: IVersionData, callback?: () => void) => { + setError(undefined); + postCreateVersion(schemaID, { + data: data, + showError: true, + setLoading: setProcessing, + onError: setError, + onSuccess: newData => { + setSchema(newData.schema); + library.localUpdateTimestamp(Number(schemaID)); + if (callback) callback(); + } + }); + }, + [schemaID, setError, library, setSchema] + ); + + const versionUpdate = useCallback( + (target: number, data: IVersionData, callback?: () => void) => { + setError(undefined); + patchVersion(String(target), { + data: data, + showError: true, + setLoading: setProcessing, + onError: setError, + onSuccess: () => { + schema!.versions = schema!.versions.map(prev => { + if (prev.id === target) { + prev.description = data.description; + prev.version = data.version; + return prev; + } else { + return prev; + } + }); + setSchema(schema); + if (callback) callback(); + } + }); + }, + [setError, schema, setSchema] + ); + + const versionDelete = useCallback( + (target: number, callback?: () => void) => { + setError(undefined); + deleteVersion(String(target), { + showError: true, + setLoading: setProcessing, + onError: setError, + onSuccess: () => { + schema!.versions = schema!.versions.filter(prev => prev.id !== target); + setSchema(schema); + if (callback) callback(); + } + }); + }, + [setError, schema, setSchema] + ); + return ( { cstRename, cstSubstitute, cstDelete, - cstMoveTo + cstMoveTo, + versionCreate, + versionUpdate, + versionDelete }} > {children} diff --git a/rsconcept/frontend/src/hooks/useRSFormDetails.ts b/rsconcept/frontend/src/hooks/useRSFormDetails.ts index 12d94cfc..b124fa42 100644 --- a/rsconcept/frontend/src/hooks/useRSFormDetails.ts +++ b/rsconcept/frontend/src/hooks/useRSFormDetails.ts @@ -7,7 +7,7 @@ import { IRSForm, IRSFormData } from '@/models/rsform'; import { loadRSFormData } from '@/models/rsformAPI'; import { getRSFormDetails } from '@/utils/backendAPI'; -function useRSFormDetails({ target }: { target?: string }) { +function useRSFormDetails({ target, version }: { target?: string; version?: string }) { const [schema, setInnerSchema] = useState(undefined); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); @@ -27,7 +27,7 @@ function useRSFormDetails({ target }: { target?: string }) { if (!target) { return; } - getRSFormDetails(target, { + getRSFormDetails(target, version ?? '', { showError: true, setLoading: setCustomLoading ?? setLoading, onError: error => { @@ -40,7 +40,7 @@ function useRSFormDetails({ target }: { target?: string }) { } }); }, - [target] + [target, version] ); useEffect(() => { diff --git a/rsconcept/frontend/src/models/library.ts b/rsconcept/frontend/src/models/library.ts index c393bbcb..7646ac2d 100644 --- a/rsconcept/frontend/src/models/library.ts +++ b/rsconcept/frontend/src/models/library.ts @@ -96,6 +96,11 @@ export interface IVersionInfo { time_create: string; } +/** + * Represents user data, intended to create or update version metadata in persistent storage. + */ +export interface IVersionData extends Omit {} + /** * Represents library item common data typical for all item types. */ diff --git a/rsconcept/frontend/src/models/rsform.ts b/rsconcept/frontend/src/models/rsform.ts index 33c00ecb..d5c381f6 100644 --- a/rsconcept/frontend/src/models/rsform.ts +++ b/rsconcept/frontend/src/models/rsform.ts @@ -205,3 +205,11 @@ export interface IRSFormUploadData { file: File; fileName: string; } + +/** + * Represents data response when creating {@link IVersionInfo}. + */ +export interface IVersionCreatedResponse { + version: number; + schema: IRSFormData; +} diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx index 01c56cfb..87553b44 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx @@ -7,6 +7,7 @@ import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useSt import { toast } from 'react-toastify'; import InfoError, { ErrorData } from '@/components/InfoError'; +import Divider from '@/components/ui/Divider'; import Loader from '@/components/ui/Loader'; import TextURL from '@/components/ui/TextURL'; import { useAccessMode } from '@/context/AccessModeContext'; @@ -439,19 +440,31 @@ export const RSEditState = ({ ) : null} {model.loading ? : null} - {model.error ? : null} + {model.error ? : null} {model.schema && !model.loading ? children : null} ); }; // ====== Internals ========= -function ProcessError({ error }: { error: ErrorData }): React.ReactElement { +function ProcessError({ + error, + isArchive, + schemaID +}: { + error: ErrorData; + isArchive: boolean; + schemaID: string; +}): React.ReactElement { if (axios.isAxiosError(error) && error.response && error.response.status === 404) { return (
-

Схема с указанным идентификатором отсутствует на портале.

- +

{`Схема с указанным идентификатором ${isArchive ? 'и версией ' : ''}отсутствует`}

+
+ + {isArchive ? : null} + {isArchive ? : null} +
); } else { diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSFormPage.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSFormPage.tsx index 6a52fc20..a386ffc7 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSFormPage.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSFormPage.tsx @@ -4,14 +4,17 @@ import { useParams } from 'react-router-dom'; import { AccessModeState } from '@/context/AccessModeContext'; import { RSFormState } from '@/context/RSFormContext'; +import useQueryStrings from '@/hooks/useQueryStrings'; import RSTabs from './RSTabs'; function RSFormPage() { const params = useParams(); + const query = useQueryStrings(); + const version = query.get('v') ?? undefined; return ( - + diff --git a/rsconcept/frontend/src/utils/backendAPI.ts b/rsconcept/frontend/src/utils/backendAPI.ts index 61119bca..1ad78a5e 100644 --- a/rsconcept/frontend/src/utils/backendAPI.ts +++ b/rsconcept/frontend/src/utils/backendAPI.ts @@ -19,7 +19,8 @@ import { IUserProfile, IUserSignupData, IUserUpdateData, - IUserUpdatePassword + IUserUpdatePassword, + IVersionData } from '@/models/library'; import { IConstituentaList, @@ -32,7 +33,8 @@ import { ICstUpdateData, IRSFormCreateData, IRSFormData, - IRSFormUploadData + IRSFormUploadData, + IVersionCreatedResponse } from '@/models/rsform'; import { IExpressionParse, IRSExpression } from '@/models/rslang'; @@ -215,12 +217,20 @@ export function postCloneLibraryItem(target: string, request: FrontExchange) { - AxiosGet({ - title: `RSForm details for id=${target}`, - endpoint: `/api/rsforms/${target}/details`, - request: request - }); +export function getRSFormDetails(target: string, version: string, request: FrontPull) { + if (!version) { + AxiosGet({ + title: `RSForm details for id=${target}`, + endpoint: `/api/rsforms/${target}/details`, + request: request + }); + } else { + AxiosGet({ + title: `RSForm details for id=${target}`, + endpoint: `/api/rsforms/${target}/versions/{version}`, + request: request + }); + } } export function patchLibraryItem(target: string, request: FrontExchange) { @@ -383,6 +393,30 @@ export function postGenerateLexeme(request: FrontExchange) { + AxiosPost({ + title: `Create version for RSForm id=${target}`, + endpoint: `/api/rsforms/${target}/versions/create`, + request: request + }); +} + +export function patchVersion(target: string, request: FrontPush) { + AxiosPatch({ + title: `Version id=${target}`, + endpoint: `/api/versions/${target}`, + request: request + }); +} + +export function deleteVersion(target: string, request: FrontAction) { + AxiosDelete({ + title: `Version id=${target}`, + endpoint: `/api/versions/${target}`, + request: request + }); +} + // ============ Helper functions ============= function AxiosGet({ endpoint, request, title, options }: IAxiosRequest) { console.log(`REQUEST: [[${title}]]`);