Add OSS scaffolding

This commit is contained in:
IRBorisov 2024-06-04 23:00:22 +03:00
parent d4d1c81bdc
commit 3f2db1e7bc
35 changed files with 1377 additions and 79 deletions

View File

@ -6,6 +6,7 @@ import LibraryPage from '@/pages/LibraryPage';
import LoginPage from '@/pages/LoginPage'; import LoginPage from '@/pages/LoginPage';
import ManualsPage from '@/pages/ManualsPage'; import ManualsPage from '@/pages/ManualsPage';
import NotFoundPage from '@/pages/NotFoundPage'; import NotFoundPage from '@/pages/NotFoundPage';
import OssPage from '@/pages/OssPage';
import PasswordChangePage from '@/pages/PasswordChangePage'; import PasswordChangePage from '@/pages/PasswordChangePage';
import RegisterPage from '@/pages/RegisterPage'; import RegisterPage from '@/pages/RegisterPage';
import RestorePasswordPage from '@/pages/RestorePasswordPage'; import RestorePasswordPage from '@/pages/RestorePasswordPage';
@ -57,6 +58,10 @@ export const Router = createBrowserRouter([
path: `${routes.rsforms}/:id`, path: `${routes.rsforms}/:id`,
element: <RSFormPage /> element: <RSFormPage />
}, },
{
path: `${routes.oss}/:id`,
element: <OssPage />
},
{ {
path: routes.manuals, path: routes.manuals,
element: <ManualsPage /> element: <ManualsPage />

View File

@ -7,8 +7,17 @@ import { toast } from 'react-toastify';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { ILexemeData, IResolutionData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language'; import { ILexemeData, IResolutionData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
import { ILibraryItem, ILibraryUpdateData, ITargetAccessPolicy, ITargetLocation, IVersionData } from '@/models/library'; import {
AccessPolicy,
ILibraryItem,
ILibraryUpdateData,
ITargetAccessPolicy,
ITargetLocation,
IVersionData,
LibraryItemType
} from '@/models/library';
import { ILibraryCreateData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss';
import { import {
IConstituentaList, IConstituentaList,
IConstituentaMeta, IConstituentaMeta,
@ -224,6 +233,29 @@ export function postCloneLibraryItem(target: string, request: FrontExchange<IRSF
}); });
} }
export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) {
request.onSuccess({
id: Number(target),
comment: '123',
alias: 'oss1',
access_policy: AccessPolicy.PUBLIC,
editors: [],
owner: 1,
item_type: LibraryItemType.OSS,
location: '/U',
read_only: false,
subscribers: [],
time_create: '0',
time_update: '0',
title: 'TestOss',
visible: false
});
// AxiosGet({
// endpoint: `/api/oss/${target}`, // TODO: endpoint to access OSS
// request: request
// });
}
export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) { export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) {
if (!version) { if (!version) {
AxiosGet({ AxiosGet({

View File

@ -17,7 +17,8 @@ export const routes = {
create_schema: 'library/create', create_schema: 'library/create',
manuals: 'manuals', manuals: 'manuals',
help: 'manuals', help: 'manuals',
rsforms: 'rsforms' rsforms: 'rsforms',
oss: 'oss'
}; };
interface SchemaProps { interface SchemaProps {
@ -27,6 +28,11 @@ interface SchemaProps {
active?: number | string; active?: number | string;
} }
interface OssProps {
id: number | string;
tab: number;
}
/** /**
* Internal navigation URLs. * Internal navigation URLs.
*/ */
@ -49,5 +55,8 @@ export const urls = {
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) => {
return `/oss/${id}?tab=${tab}`;
} }
}; };

View File

@ -0,0 +1,275 @@
'use client';
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import {
type DataCallback,
deleteUnsubscribe,
patchEditorsSet as patchSetEditors,
patchLibraryItem,
patchSetAccessPolicy,
patchSetLocation,
patchSetOwner,
postSubscribe
} from '@/app/backendAPI';
import { type ErrorData } from '@/components/info/InfoError';
import useOssDetails from '@/hooks/useOssDetails';
import { AccessPolicy, ILibraryItem } from '@/models/library';
import { ILibraryUpdateData } from '@/models/library';
import { IOperationSchema } from '@/models/oss';
import { UserID } from '@/models/user';
import { useAuth } from './AuthContext';
import { useLibrary } from './LibraryContext';
interface IOssContext {
schema?: IOperationSchema;
itemID: string;
loading: boolean;
errorLoading: ErrorData;
processing: boolean;
processingError: ErrorData;
isOwned: boolean;
isSubscribed: boolean;
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
subscribe: (callback?: () => void) => void;
unsubscribe: (callback?: () => void) => void;
setOwner: (newOwner: UserID, callback?: () => void) => void;
setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
setLocation: (newLocation: string, callback?: () => void) => void;
setEditors: (newEditors: UserID[], callback?: () => void) => void;
}
const OssContext = createContext<IOssContext | null>(null);
export const useOSS = () => {
const context = useContext(OssContext);
if (context === null) {
throw new Error('useOSS has to be used within <OssState.Provider>');
}
return context;
};
interface OssStateProps {
itemID: string;
children: React.ReactNode;
}
export const OssState = ({ itemID, children }: OssStateProps) => {
const library = useLibrary();
const { user } = useAuth();
const {
schema: schema, // prettier: split lines
error: errorLoading,
setSchema,
loading
} = useOssDetails({
target: itemID
});
const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
const [toggleTracking, setToggleTracking] = useState(false);
const isOwned = useMemo(() => {
return user?.id === schema?.owner || false;
}, [user, schema?.owner]);
const isSubscribed = useMemo(() => {
if (!user || !schema || !user.id) {
return false;
}
return schema.subscribers.includes(user.id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, schema, toggleTracking]);
const update = useCallback(
(data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => {
if (!schema) {
return;
}
setProcessingError(undefined);
patchLibraryItem(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
setSchema(Object.assign(schema, newData));
library.localUpdateItem(newData);
if (callback) callback(newData);
}
});
},
[itemID, setSchema, schema, library]
);
const subscribe = useCallback(
(callback?: () => void) => {
if (!schema || !user) {
return;
}
setProcessingError(undefined);
postSubscribe(itemID, {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
if (user.id && !schema.subscribers.includes(user.id)) {
schema.subscribers.push(user.id);
}
if (!user.subscriptions.includes(schema.id)) {
user.subscriptions.push(schema.id);
}
setToggleTracking(prev => !prev);
if (callback) callback();
}
});
},
[itemID, schema, user]
);
const unsubscribe = useCallback(
(callback?: () => void) => {
if (!schema || !user) {
return;
}
setProcessingError(undefined);
deleteUnsubscribe(itemID, {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
if (user.id && schema.subscribers.includes(user.id)) {
schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1);
}
if (user.subscriptions.includes(schema.id)) {
user.subscriptions.splice(user.subscriptions.indexOf(schema.id), 1);
}
setToggleTracking(prev => !prev);
if (callback) callback();
}
});
},
[itemID, schema, user]
);
const setOwner = useCallback(
(newOwner: UserID, callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
patchSetOwner(itemID, {
data: {
user: newOwner
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
schema.owner = newOwner;
library.localUpdateItem(schema);
if (callback) callback();
}
});
},
[itemID, schema, library]
);
const setAccessPolicy = useCallback(
(newPolicy: AccessPolicy, callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
patchSetAccessPolicy(itemID, {
data: {
access_policy: newPolicy
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
schema.access_policy = newPolicy;
library.localUpdateItem(schema);
if (callback) callback();
}
});
},
[itemID, schema, library]
);
const setLocation = useCallback(
(newLocation: string, callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
patchSetLocation(itemID, {
data: {
location: newLocation
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
schema.location = newLocation;
library.localUpdateItem(schema);
if (callback) callback();
}
});
},
[itemID, schema, library]
);
const setEditors = useCallback(
(newEditors: UserID[], callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
patchSetEditors(itemID, {
data: {
users: newEditors
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
schema.editors = newEditors;
if (callback) callback();
}
});
},
[itemID, schema]
);
return (
<OssContext.Provider
value={{
schema,
itemID,
loading,
errorLoading,
processing,
processingError,
isOwned,
isSubscribed,
update,
subscribe,
unsubscribe,
setOwner,
setEditors,
setAccessPolicy,
setLocation
}}
>
{children}
</OssContext.Provider>
);
};

View File

@ -54,7 +54,7 @@ import { useLibrary } from './LibraryContext';
interface IRSFormContext { interface IRSFormContext {
schema?: IRSForm; schema?: IRSForm;
schemaID: string; itemID: string;
versionID?: string; versionID?: string;
loading: boolean; loading: boolean;
@ -105,12 +105,12 @@ export const useRSForm = () => {
}; };
interface RSFormStateProps { interface RSFormStateProps {
schemaID: string; itemID: string;
versionID?: string; versionID?: string;
children: React.ReactNode; children: React.ReactNode;
} }
export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps) => { export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) => {
const library = useLibrary(); const library = useLibrary();
const { user } = useAuth(); const { user } = useAuth();
const { const {
@ -120,7 +120,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
setSchema, setSchema,
loading loading
} = useRSFormDetails({ } = useRSFormDetails({
target: schemaID, target: itemID,
version: versionID version: versionID
}); });
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
@ -148,7 +148,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
patchLibraryItem(schemaID, { patchLibraryItem(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
@ -160,7 +160,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, setSchema, schema, library] [itemID, setSchema, schema, library]
); );
const upload = useCallback( const upload = useCallback(
@ -169,7 +169,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
patchUploadTRS(schemaID, { patchUploadTRS(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
@ -181,7 +181,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, setSchema, schema, library] [itemID, setSchema, schema, library]
); );
const subscribe = useCallback( const subscribe = useCallback(
@ -190,7 +190,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
postSubscribe(schemaID, { postSubscribe(itemID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
@ -206,7 +206,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, schema, user] [itemID, schema, user]
); );
const unsubscribe = useCallback( const unsubscribe = useCallback(
@ -215,7 +215,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
deleteUnsubscribe(schemaID, { deleteUnsubscribe(itemID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
@ -231,7 +231,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, schema, user] [itemID, schema, user]
); );
const setOwner = useCallback( const setOwner = useCallback(
@ -240,7 +240,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
patchSetOwner(schemaID, { patchSetOwner(itemID, {
data: { data: {
user: newOwner user: newOwner
}, },
@ -254,7 +254,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, schema, library] [itemID, schema, library]
); );
const setAccessPolicy = useCallback( const setAccessPolicy = useCallback(
@ -263,7 +263,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
patchSetAccessPolicy(schemaID, { patchSetAccessPolicy(itemID, {
data: { data: {
access_policy: newPolicy access_policy: newPolicy
}, },
@ -277,7 +277,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, schema, library] [itemID, schema, library]
); );
const setLocation = useCallback( const setLocation = useCallback(
@ -286,7 +286,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
patchSetLocation(schemaID, { patchSetLocation(itemID, {
data: { data: {
location: newLocation location: newLocation
}, },
@ -300,7 +300,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, schema, library] [itemID, schema, library]
); );
const setEditors = useCallback( const setEditors = useCallback(
@ -309,7 +309,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
patchSetEditors(schemaID, { patchSetEditors(itemID, {
data: { data: {
users: newEditors users: newEditors
}, },
@ -322,7 +322,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, schema] [itemID, schema]
); );
const resetAliases = useCallback( const resetAliases = useCallback(
@ -331,7 +331,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
patchResetAliases(schemaID, { patchResetAliases(itemID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
@ -342,7 +342,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, schema, library, user, setSchema] [itemID, schema, library, user, setSchema]
); );
const restoreOrder = useCallback( const restoreOrder = useCallback(
@ -351,7 +351,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
patchRestoreOrder(schemaID, { patchRestoreOrder(itemID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
@ -362,13 +362,13 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, schema, library, user, setSchema] [itemID, schema, library, user, setSchema]
); );
const produceStructure = useCallback( const produceStructure = useCallback(
(data: ITargetCst, callback?: DataCallback<ConstituentaID[]>) => { (data: ITargetCst, callback?: DataCallback<ConstituentaID[]>) => {
setProcessingError(undefined); setProcessingError(undefined);
patchProduceStructure(schemaID, { patchProduceStructure(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
@ -380,26 +380,26 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[setSchema, library, schemaID] [setSchema, library, itemID]
); );
const download = useCallback( const download = useCallback(
(callback: DataCallback<Blob>) => { (callback: DataCallback<Blob>) => {
setProcessingError(undefined); setProcessingError(undefined);
getTRSFile(schemaID, String(schema?.version ?? ''), { getTRSFile(itemID, String(schema?.version ?? ''), {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: callback onSuccess: callback
}); });
}, },
[schemaID, schema] [itemID, schema]
); );
const cstCreate = useCallback( const cstCreate = useCallback(
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => { (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
setProcessingError(undefined); setProcessingError(undefined);
postNewConstituenta(schemaID, { postNewConstituenta(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
@ -411,13 +411,13 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, library, setSchema] [itemID, library, setSchema]
); );
const cstDelete = useCallback( const cstDelete = useCallback(
(data: IConstituentaList, callback?: () => void) => { (data: IConstituentaList, callback?: () => void) => {
setProcessingError(undefined); setProcessingError(undefined);
patchDeleteConstituenta(schemaID, { patchDeleteConstituenta(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
@ -429,7 +429,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[schemaID, library, setSchema] [itemID, library, setSchema]
); );
const cstUpdate = useCallback( const cstUpdate = useCallback(
@ -442,18 +442,18 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => onSuccess: newData =>
reload(setProcessing, () => { reload(setProcessing, () => {
library.localUpdateTimestamp(Number(schemaID)); library.localUpdateTimestamp(Number(itemID));
if (callback) callback(newData); if (callback) callback(newData);
}) })
}); });
}, },
[schemaID, library, reload] [itemID, library, reload]
); );
const cstRename = useCallback( const cstRename = useCallback(
(data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => { (data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => {
setProcessingError(undefined); setProcessingError(undefined);
patchRenameConstituenta(schemaID, { patchRenameConstituenta(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
@ -465,13 +465,13 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[setSchema, library, schemaID] [setSchema, library, itemID]
); );
const cstSubstitute = useCallback( const cstSubstitute = useCallback(
(data: ICstSubstituteData, callback?: () => void) => { (data: ICstSubstituteData, callback?: () => void) => {
setProcessingError(undefined); setProcessingError(undefined);
patchSubstituteConstituents(schemaID, { patchSubstituteConstituents(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
@ -483,43 +483,43 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
} }
}); });
}, },
[setSchema, library, schemaID] [setSchema, library, itemID]
); );
const cstMoveTo = useCallback( const cstMoveTo = useCallback(
(data: ICstMovetoData, callback?: () => void) => { (data: ICstMovetoData, callback?: () => void) => {
setProcessingError(undefined); setProcessingError(undefined);
patchMoveConstituenta(schemaID, { patchMoveConstituenta(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(Number(schemaID)); library.localUpdateTimestamp(Number(itemID));
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[schemaID, library, setSchema] [itemID, library, setSchema]
); );
const versionCreate = useCallback( const versionCreate = useCallback(
(data: IVersionData, callback?: (version: number) => void) => { (data: IVersionData, callback?: (version: number) => void) => {
setProcessingError(undefined); setProcessingError(undefined);
postCreateVersion(schemaID, { postCreateVersion(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(Number(schemaID)); library.localUpdateTimestamp(Number(itemID));
if (callback) callback(newData.version); if (callback) callback(newData.version);
} }
}); });
}, },
[schemaID, library, setSchema] [itemID, library, setSchema]
); );
const versionUpdate = useCallback( const versionUpdate = useCallback(
@ -592,19 +592,19 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(Number(schemaID)); library.localUpdateTimestamp(Number(itemID));
if (callback) callback(newData); if (callback) callback(newData);
} }
}); });
}, },
[library, schemaID, setSchema] [library, itemID, setSchema]
); );
return ( return (
<RSFormContext.Provider <RSFormContext.Provider
value={{ value={{
schema, schema,
schemaID, itemID,
versionID, versionID,
loading, loading,
errorLoading, errorLoading,

View File

@ -0,0 +1,53 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { getOssDetails } from '@/app/backendAPI';
import { type ErrorData } from '@/components/info/InfoError';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { OssLoader } from '@/models/OssLoader';
function useOssDetails({ target }: { target?: string }) {
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorData>(undefined);
function setSchema(data?: IOperationSchemaData) {
if (!data) {
setInner(undefined);
return;
}
const newSchema = new OssLoader(data).produceOSS();
setInner(newSchema);
}
const reload = useCallback(
(setCustomLoading?: typeof setLoading, callback?: () => void) => {
setError(undefined);
if (!target) {
return;
}
getOssDetails(target, {
showError: true,
setLoading: setCustomLoading ?? setLoading,
onError: error => {
setInner(undefined);
setError(error);
},
onSuccess: schema => {
setSchema(schema);
if (callback) callback();
}
});
},
[target]
);
useEffect(() => {
reload();
}, [reload]);
return { schema, setSchema, reload, error, setError, loading };
}
export default useOssDetails;

View File

@ -0,0 +1,23 @@
/**
* Module: OSS data loading and processing.
*/
import { IOperationSchema, IOperationSchemaData } from './oss';
/**
* Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaData}.
*
*/
export class OssLoader {
private schema: IOperationSchemaData;
constructor(input: IOperationSchemaData) {
this.schema = input;
}
produceOSS(): IOperationSchema {
const result = this.schema as IOperationSchema;
result.producedData = [1, 2, 3]; // TODO: put data processing here
return result;
}
}

View File

@ -75,15 +75,39 @@ export interface ILibraryItem {
} }
/** /**
* Represents library item extended data. * Represents library item constant data loaded for both OSS and RSForm.
*/ */
export interface ILibraryItemEx extends ILibraryItem { export interface ILibraryItemData extends ILibraryItem {
subscribers: UserID[]; subscribers: UserID[];
editors: UserID[]; editors: UserID[];
}
/**
* Represents library item extended data with versions.
*/
export interface ILibraryItemVersioned extends ILibraryItemData {
version?: VersionID; version?: VersionID;
versions: IVersionInfo[]; versions: IVersionInfo[];
} }
/**
* Represents common library item editor controller.
*/
export interface ILibraryItemEditor {
schema?: ILibraryItemData;
isMutable: boolean;
isProcessing: boolean;
setOwner: (newOwner: UserID) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void;
promptLocation: () => void;
toggleSubscribe: () => void;
share: () => void;
}
/** /**
* Represents update data for editing {@link ILibraryItem}. * Represents update data for editing {@link ILibraryItem}.
*/ */

View File

@ -0,0 +1,23 @@
/**
* Module: Schema of Synthesis Operations.
*/
import { ILibraryItemData } from './library';
import { UserID } from './user';
/**
* Represents backend data for Schema of Synthesis Operations.
*/
export interface IOperationSchemaData extends ILibraryItemData {
additional_data?: number[];
}
/**
* Represents Schema of Synthesis Operations.
*/
export interface IOperationSchema extends IOperationSchemaData {
subscribers: UserID[];
editors: UserID[];
producedData: number[]; // TODO: modify this to store calculated state on load
}

View File

@ -4,7 +4,7 @@
import { Graph } from '@/models/Graph'; import { Graph } from '@/models/Graph';
import { ILibraryItem, ILibraryItemEx, LibraryItemID } from './library'; import { ILibraryItem, ILibraryItemVersioned, LibraryItemID } from './library';
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang'; import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang';
/** /**
@ -226,7 +226,7 @@ export interface IRSFormStats {
/** /**
* Represents formal explication for set of concepts. * Represents formal explication for set of concepts.
*/ */
export interface IRSForm extends ILibraryItemEx { export interface IRSForm extends ILibraryItemVersioned {
items: IConstituenta[]; items: IConstituenta[];
stats: IRSFormStats; stats: IRSFormStats;
graph: Graph; graph: Graph;
@ -237,7 +237,7 @@ export interface IRSForm extends ILibraryItemEx {
/** /**
* Represents data for {@link IRSForm} provided by backend. * Represents data for {@link IRSForm} provided by backend.
*/ */
export interface IRSFormData extends ILibraryItemEx { export interface IRSFormData extends ILibraryItemVersioned {
items: IConstituentaData[]; items: IConstituentaData[];
} }

View File

@ -15,7 +15,7 @@ import { useConceptOptions } from '@/context/OptionsContext';
import { useUsers } from '@/context/UsersContext'; import { useUsers } from '@/context/UsersContext';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { ILibraryItem } from '@/models/library'; import { ILibraryItem, LibraryItemType } from '@/models/library';
import { storage } from '@/utils/constants'; import { storage } from '@/utils/constants';
interface ViewLibraryProps { interface ViewLibraryProps {
@ -33,7 +33,11 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50); const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50);
function handleOpenItem(item: ILibraryItem, event: CProps.EventMouse) { function handleOpenItem(item: ILibraryItem, event: CProps.EventMouse) {
if (item.item_type === LibraryItemType.RSFORM) {
router.push(urls.schema(item.id), event.ctrlKey || event.metaKey); router.push(urls.schema(item.id), event.ctrlKey || event.metaKey);
} else if (item.item_type === LibraryItemType.OSS) {
router.push(urls.oss(item.id), event.ctrlKey || event.metaKey);
}
} }
const windowSize = useWindowSize(); const windowSize = useWindowSize();

View File

@ -1,7 +1,7 @@
function HelpAccess() { function HelpAccess() {
return ( return (
<div> <div>
<h1>Организация доступов к схемам</h1> <h1>Организация доступов</h1>
<p>TBD.</p> <p>TBD.</p>
</div> </div>
); );

View File

@ -0,0 +1,62 @@
'use client';
import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useAuth } from '@/context/AuthContext';
import { useOSS } from '@/context/OssContext';
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
import { globals } from '@/utils/constants';
import { useOssEdit } from '../OssEditContext';
import FormOSS from './FormOSS';
import RSFormToolbar from './OssFormToolbar';
interface EditorOssCardProps {
isModified: boolean;
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
onDestroy: () => void;
}
function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardProps) {
const { schema, isSubscribed } = useOSS();
const { user } = useAuth();
const controller = useOssEdit();
function initiateSubmit() {
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
if (element) {
element.requestSubmit();
}
}
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
if (isModified) {
initiateSubmit();
}
event.preventDefault();
}
}
return (
<>
<RSFormToolbar
subscribed={isSubscribed}
modified={isModified}
anonymous={!user}
onSubmit={initiateSubmit}
onDestroy={onDestroy}
/>
<AnimateFade onKeyDown={handleInput} className={clsx('sm:w-fit mx-auto', 'flex flex-col sm:flex-row')}>
<FlexColumn className='px-3'>
<FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
<EditorLibraryItem item={schema} isModified={isModified} controller={controller} />
</FlexColumn>
</AnimateFade>
</>
);
}
export default EditorOssCard;

View File

@ -0,0 +1,140 @@
'use client';
import clsx from 'clsx';
import { useEffect, useLayoutEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { IconSave } from '@/components/Icons';
import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import { useOSS } from '@/context/OssContext';
import { ILibraryUpdateData, LibraryItemType } from '@/models/library';
import AccessToolbar from '@/pages/RSFormPage/EditorRSFormCard/AccessToolbar';
import { limits, patterns } from '@/utils/constants';
import { useOssEdit } from '../OssEditContext';
interface FormOSSProps {
id?: string;
isModified: boolean;
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
}
function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
const { schema, update, processing } = useOSS();
const controller = useOssEdit();
const [title, setTitle] = useState('');
const [alias, setAlias] = useState('');
const [comment, setComment] = useState('');
const [visible, setVisible] = useState(false);
const [readOnly, setReadOnly] = useState(false);
useEffect(() => {
if (!schema) {
setIsModified(false);
return;
}
setIsModified(
schema.title !== title ||
schema.alias !== alias ||
schema.comment !== comment ||
schema.visible !== visible ||
schema.read_only !== readOnly
);
return () => setIsModified(false);
}, [
schema,
schema?.title,
schema?.alias,
schema?.comment,
schema?.visible,
schema?.read_only,
title,
alias,
comment,
visible,
readOnly,
setIsModified
]);
useLayoutEffect(() => {
if (schema) {
setTitle(schema.title);
setAlias(schema.alias);
setComment(schema.comment);
setVisible(schema.visible);
setReadOnly(schema.read_only);
}
}, [schema]);
const handleSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
if (event) {
event.preventDefault();
}
const data: ILibraryUpdateData = {
item_type: LibraryItemType.RSFORM,
title: title,
alias: alias,
comment: comment,
visible: visible,
read_only: readOnly
};
update(data, () => toast.success('Изменения сохранены'));
};
return (
<form id={id} className={clsx('mt-1 min-w-[22rem] sm:w-[30rem]', 'flex flex-col pt-1')} onSubmit={handleSubmit}>
<TextInput
id='schema_title'
required
label='Полное название'
className='mb-3'
value={title}
disabled={!controller.isMutable}
onChange={event => setTitle(event.target.value)}
/>
<div className='flex justify-between w-full gap-3 mb-3'>
<TextInput
id='schema_alias'
required
label='Сокращение'
className='w-[14rem]'
pattern={patterns.library_alias}
title={`не более ${limits.library_alias_len} символов`}
disabled={!controller.isMutable}
value={alias}
onChange={event => setAlias(event.target.value)}
/>
<AccessToolbar
visible={visible}
toggleVisible={() => setVisible(prev => !prev)}
readOnly={readOnly}
toggleReadOnly={() => setReadOnly(prev => !prev)}
controller={controller}
/>
</div>
<TextArea
id='schema_comment'
label='Описание'
rows={3}
value={comment}
disabled={!controller.isMutable || controller.isProcessing}
onChange={event => setComment(event.target.value)}
/>
{controller.isMutable || isModified ? (
<SubmitButton
text='Сохранить изменения'
className='self-center mt-4'
loading={processing}
disabled={!isModified}
icon={<IconSave size='1.25rem' />}
/>
) : null}
</form>
);
}
export default FormOSS;

View File

@ -0,0 +1,65 @@
'use client';
import { useMemo } from 'react';
import { SubscribeIcon } from '@/components/DomainIcons';
import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import { useAccessMode } from '@/context/AccessModeContext';
import { HelpTopic } from '@/models/miscellaneous';
import { UserLevel } from '@/models/user';
import { prepareTooltip } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext';
interface RSFormToolbarProps {
modified: boolean;
subscribed: boolean;
anonymous: boolean;
onSubmit: () => void;
onDestroy: () => void;
}
function RSFormToolbar({ modified, anonymous, subscribed, onSubmit, onDestroy }: RSFormToolbarProps) {
const controller = useOssEdit();
const { accessLevel } = useAccessMode();
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
return (
<Overlay position='top-1 right-1/2 translate-x-1/2' className='cc-icons'>
{controller.isMutable || modified ? (
<MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
disabled={!canSave}
icon={<IconSave size='1.25rem' className='icon-primary' />}
onClick={onSubmit}
/>
) : null}
<MiniButton
title='Поделиться схемой'
icon={<IconShare size='1.25rem' className='icon-primary' />}
onClick={controller.share}
/>
{!anonymous ? (
<MiniButton
titleHtml={`Отслеживание <b>${subscribed ? 'включено' : 'выключено'}</b>`}
icon={<SubscribeIcon value={subscribed} className={subscribed ? 'icon-primary' : 'clr-text-controls'} />}
disabled={controller.isProcessing}
onClick={controller.toggleSubscribe}
/>
) : null}
{controller.isMutable ? (
<MiniButton
title='Удалить схему'
icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={!controller.isMutable || controller.isProcessing || accessLevel < UserLevel.OWNER}
onClick={onDestroy}
/>
) : null}
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} className='max-w-[30rem]' />
</Overlay>
);
}
export default RSFormToolbar;

View File

@ -0,0 +1 @@
export { default } from './EditorOssCard';

View File

@ -0,0 +1,15 @@
'use client';
import AnimateFade from '@/components/wrap/AnimateFade';
function EditorOssGraph() {
// TODO: Implement OSS editing UI here
return (
<AnimateFade>
<div className='py-3'>Реализация графического интерфейса</div>
</AnimateFade>
);
}
export default EditorOssGraph;

View File

@ -0,0 +1 @@
export { default } from './EditorOssGraph';

View File

@ -0,0 +1,198 @@
'use client';
import axios from 'axios';
import { AnimatePresence } from 'framer-motion';
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import Loader from '@/components/ui/Loader';
import TextURL from '@/components/ui/TextURL';
import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/OptionsContext';
import { useOSS } from '@/context/OssContext';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
import DlgEditEditors from '@/dialogs/DlgEditEditors';
import { AccessPolicy } from '@/models/library';
import { IOperationSchema } from '@/models/oss';
import { UserID, UserLevel } from '@/models/user';
interface IOssEditContext {
schema?: IOperationSchema;
isMutable: boolean;
isProcessing: boolean;
setOwner: (newOwner: UserID) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void;
promptLocation: () => void;
toggleSubscribe: () => void;
share: () => void;
}
const OssEditContext = createContext<IOssEditContext | null>(null);
export const useOssEdit = () => {
const context = useContext(OssEditContext);
if (context === null) {
throw new Error('useOssEdit has to be used within <OssEditState.Provider>');
}
return context;
};
interface OssEditStateProps {
// isModified: boolean;
children: React.ReactNode;
}
export const OssEditState = ({ children }: OssEditStateProps) => {
// const router = useConceptNavigation();
const { user } = useAuth();
const { adminMode } = useConceptOptions();
const { accessLevel, setAccessLevel } = useAccessMode();
const model = useOSS();
const isMutable = useMemo(
() => accessLevel > UserLevel.READER && !model.schema?.read_only,
[accessLevel, model.schema?.read_only]
);
const [showEditEditors, setShowEditEditors] = useState(false);
const [showEditLocation, setShowEditLocation] = useState(false);
useLayoutEffect(
() =>
setAccessLevel(prev => {
if (
prev === UserLevel.EDITOR &&
(model.isOwned || user?.is_staff || (user && model.schema?.editors.includes(user.id)))
) {
return UserLevel.EDITOR;
} else if (user?.is_staff && (prev === UserLevel.ADMIN || adminMode)) {
return UserLevel.ADMIN;
} else if (model.isOwned) {
return UserLevel.OWNER;
} else if (user?.id && model.schema?.editors.includes(user?.id)) {
return UserLevel.EDITOR;
} else {
return UserLevel.READER;
}
}),
[model.schema, setAccessLevel, model.isOwned, user, adminMode]
);
const handleSetLocation = useCallback(
(newLocation: string) => {
if (!model.schema) {
return;
}
model.setLocation(newLocation, () => toast.success('Схема перемещена'));
},
[model]
);
const promptEditors = useCallback(() => {
setShowEditEditors(true);
}, []);
const promptLocation = useCallback(() => {
setShowEditLocation(true);
}, []);
const share = useCallback(() => {
const currentRef = window.location.href;
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
navigator.clipboard
.writeText(url)
.then(() => toast.success(`Ссылка скопирована: ${url}`))
.catch(console.error);
}, []);
const toggleSubscribe = useCallback(() => {
if (model.isSubscribed) {
model.unsubscribe(() => toast.success('Отслеживание отключено'));
} else {
model.subscribe(() => toast.success('Отслеживание включено'));
}
}, [model]);
const setOwner = useCallback(
(newOwner: UserID) => {
model.setOwner(newOwner, () => toast.success('Владелец обновлен'));
},
[model]
);
const setAccessPolicy = useCallback(
(newPolicy: AccessPolicy) => {
model.setAccessPolicy(newPolicy, () => toast.success('Политика доступа изменена'));
},
[model]
);
const setEditors = useCallback(
(newEditors: UserID[]) => {
model.setEditors(newEditors, () => toast.success('Редакторы обновлены'));
},
[model]
);
return (
<OssEditContext.Provider
value={{
schema: model.schema,
isMutable,
isProcessing: model.processing,
toggleSubscribe,
setOwner,
setAccessPolicy,
promptEditors,
promptLocation,
share
}}
>
{model.schema ? (
<AnimatePresence>
{showEditEditors ? (
<DlgEditEditors
hideWindow={() => setShowEditEditors(false)}
editors={model.schema.editors}
setEditors={setEditors}
/>
) : null}
{showEditLocation ? (
<DlgChangeLocation
hideWindow={() => setShowEditLocation(false)}
initial={model.schema.location}
onChangeLocation={handleSetLocation}
/>
) : null}
</AnimatePresence>
) : null}
{model.loading ? <Loader /> : null}
{model.errorLoading ? <ProcessError error={model.errorLoading} /> : null}
{model.schema && !model.loading ? children : null}
</OssEditContext.Provider>
);
};
// ====== Internals =========
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
return (
<div className='p-2 text-center'>
<p>{`Схема с указанным идентификатором отсутствует`}</p>
<div className='flex justify-center'>
<TextURL text='Библиотека' href='/library' />
</div>
</div>
);
} else {
return <InfoError error={error} />;
}
}

View File

@ -0,0 +1,21 @@
'use client';
import { useParams } from 'react-router-dom';
import { AccessModeState } from '@/context/AccessModeContext';
import { OssState } from '@/context/OssContext';
import OssTabs from './OssTabs';
function OssPage() {
const params = useParams();
return (
<AccessModeState>
<OssState itemID={params.id ?? ''}>
<OssTabs />
</OssState>
</AccessModeState>
);
}
export default OssPage;

View File

@ -0,0 +1,143 @@
'use client';
import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import TabLabel from '@/components/ui/TabLabel';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext';
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
import { useConceptOptions } from '@/context/OptionsContext';
import { useOSS } from '@/context/OssContext';
import useQueryStrings from '@/hooks/useQueryStrings';
import EditorRSForm from './EditorOssCard';
import EditorTermGraph from './EditorOssGraph';
import { OssEditState } from './OssEditContext';
import OssTabsMenu from './OssTabsMenu';
export enum OssTabID {
CARD = 0,
GRAPH = 1
}
function OssTabs() {
const router = useConceptNavigation();
const query = useQueryStrings();
const activeTab = (Number(query.get('tab')) ?? OssTabID.CARD) as OssTabID;
const { calculateHeight } = useConceptOptions();
const { schema, loading } = useOSS();
const { destroyItem } = useLibrary();
const [isModified, setIsModified] = useState(false);
useBlockNavigation(isModified);
useLayoutEffect(() => {
if (schema) {
const oldTitle = document.title;
document.title = schema.title;
return () => {
document.title = oldTitle;
};
}
}, [schema, schema?.title]);
const navigateTab = useCallback(
(tab: OssTabID) => {
if (!schema) {
return;
}
const url = urls.oss_props({
id: schema.id,
tab: tab
});
router.push(url);
},
[router, schema]
);
function onSelectTab(index: number, last: number, event: Event) {
if (last === index) {
return;
}
if (event.type == 'keydown') {
const kbEvent = event as KeyboardEvent;
if (kbEvent.altKey) {
if (kbEvent.code === 'ArrowLeft') {
router.back();
return;
} else if (kbEvent.code === 'ArrowRight') {
router.forward();
return;
}
}
}
navigateTab(index);
}
const onDestroySchema = useCallback(() => {
if (!schema || !window.confirm('Вы уверены, что хотите удалить данную схему?')) {
return;
}
destroyItem(schema.id, () => {
toast.success('Схема удалена');
router.push(urls.library);
});
}, [schema, destroyItem, router]);
const panelHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
const cardPanel = useMemo(
() => (
<TabPanel>
<EditorRSForm
isModified={isModified} // prettier: split lines
setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
),
[isModified, onDestroySchema]
);
const graphPanel = useMemo(
() => (
<TabPanel>
<EditorTermGraph />
</TabPanel>
),
[]
);
return (
<OssEditState>
{schema && !loading ? (
<Tabs
selectedIndex={activeTab}
onSelect={onSelectTab}
defaultFocus
selectedTabClassName='clr-selected'
className='flex flex-col mx-auto min-w-fit'
>
<TabList className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2')}>
<OssTabsMenu onDestroy={onDestroySchema} />
<TabLabel label='Карточка' titleHtml={`Название: <b>${schema.title ?? ''}</b>`} />
<TabLabel label='Граф' />
</TabList>
<AnimateFade className='overflow-y-auto' style={{ maxHeight: panelHeight }}>
{cardPanel}
{graphPanel}
</AnimateFade>
</Tabs>
) : null}
</OssEditState>
);
}
export default OssTabs;

View File

@ -0,0 +1,203 @@
'use client';
import { urls } from '@/app/urls';
import {
IconAdmin,
IconAlert,
IconDestroy,
IconEdit2,
IconEditor,
IconLibrary,
IconMenu,
IconNewItem,
IconOwner,
IconReader,
IconShare
} from '@/components/Icons';
import Button from '@/components/ui/Button';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext';
import useDropdown from '@/hooks/useDropdown';
import { UserLevel } from '@/models/user';
import { describeAccessMode, labelAccessMode } from '@/utils/labels';
import { useOssEdit } from './OssEditContext';
interface OssTabsMenuProps {
onDestroy: () => void;
}
function OssTabsMenu({ onDestroy }: OssTabsMenuProps) {
const controller = useOssEdit();
const router = useConceptNavigation();
const { user } = useAuth();
const model = useOSS();
const { accessLevel, setAccessLevel } = useAccessMode();
const schemaMenu = useDropdown();
const editMenu = useDropdown();
const accessMenu = useDropdown();
function handleDelete() {
schemaMenu.hide();
onDestroy();
}
function handleShare() {
schemaMenu.hide();
controller.share();
}
function handleChangeMode(newMode: UserLevel) {
accessMenu.hide();
setAccessLevel(newMode);
}
function handleCreateNew() {
router.push(urls.create_schema);
}
function handleLogin() {
router.push(urls.login);
}
return (
<div className='flex'>
<div ref={schemaMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title='Меню'
hideTitle={schemaMenu.isOpen}
icon={<IconMenu size='1.25rem' className='clr-text-controls' />}
className='h-full pl-2'
onClick={schemaMenu.toggle}
/>
<Dropdown isOpen={schemaMenu.isOpen}>
<DropdownButton
text='Поделиться'
icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare}
/>
{controller.isMutable ? (
<DropdownButton
text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={controller.isProcessing || accessLevel < UserLevel.OWNER}
onClick={handleDelete}
/>
) : null}
{user ? (
<DropdownButton
className='border-t-2'
text='Создать новую схему'
icon={<IconNewItem size='1rem' className='icon-primary' />}
onClick={handleCreateNew}
/>
) : null}
<DropdownButton
text='Библиотека'
icon={<IconLibrary size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.library)}
/>
</Dropdown>
</div>
{user ? (
<div ref={editMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title={'Редактирование'}
hideTitle={editMenu.isOpen}
className='h-full px-2'
icon={<IconEdit2 size='1.25rem' className={controller.isMutable ? 'icon-green' : 'icon-red'} />}
onClick={editMenu.toggle}
/>
<Dropdown isOpen={editMenu.isOpen}>
<div>операции над ОСС</div>
</Dropdown>
</div>
) : null}
{user ? (
<div ref={accessMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title={`Режим ${labelAccessMode(accessLevel)}`}
hideTitle={accessMenu.isOpen}
className='h-full pr-2'
icon={
accessLevel === UserLevel.ADMIN ? (
<IconAdmin size='1.25rem' className='icon-primary' />
) : accessLevel === UserLevel.OWNER ? (
<IconOwner size='1.25rem' className='icon-primary' />
) : accessLevel === UserLevel.EDITOR ? (
<IconEditor size='1.25rem' className='icon-primary' />
) : (
<IconReader size='1.25rem' className='icon-primary' />
)
}
onClick={accessMenu.toggle}
/>
<Dropdown isOpen={accessMenu.isOpen}>
<DropdownButton
text={labelAccessMode(UserLevel.READER)}
title={describeAccessMode(UserLevel.READER)}
icon={<IconReader size='1rem' className='icon-primary' />}
onClick={() => handleChangeMode(UserLevel.READER)}
/>
<DropdownButton
text={labelAccessMode(UserLevel.EDITOR)}
title={describeAccessMode(UserLevel.EDITOR)}
icon={<IconEditor size='1rem' className='icon-primary' />}
disabled={!model.isOwned && !model.schema?.editors.includes(user.id)}
onClick={() => handleChangeMode(UserLevel.EDITOR)}
/>
<DropdownButton
text={labelAccessMode(UserLevel.OWNER)}
title={describeAccessMode(UserLevel.OWNER)}
icon={<IconOwner size='1rem' className='icon-primary' />}
disabled={!model.isOwned}
onClick={() => handleChangeMode(UserLevel.OWNER)}
/>
<DropdownButton
text={labelAccessMode(UserLevel.ADMIN)}
title={describeAccessMode(UserLevel.ADMIN)}
icon={<IconAdmin size='1rem' className='icon-primary' />}
disabled={!user?.is_staff}
onClick={() => handleChangeMode(UserLevel.ADMIN)}
/>
</Dropdown>
</div>
) : null}
{!user ? (
<Button
dense
noBorder
noOutline
tabIndex={-1}
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
hideTitle={accessMenu.isOpen}
className='h-full pr-2'
icon={<IconAlert size='1.25rem' className='icon-red' />}
onClick={handleLogin}
/>
) : null}
</div>
);
}
export default OssTabsMenu;

View File

@ -0,0 +1 @@
export { default } from './OssPage';

View File

@ -1 +0,0 @@
export { default } from './EditorRSForm';

View File

@ -8,21 +8,19 @@ import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { AccessPolicy } from '@/models/library'; import { AccessPolicy, ILibraryItemEditor } from '@/models/library';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { UserLevel } from '@/models/user'; import { UserLevel } from '@/models/user';
import { useRSEdit } from '../RSEditContext';
interface AccessToolbarProps { interface AccessToolbarProps {
visible: boolean; visible: boolean;
toggleVisible: () => void; toggleVisible: () => void;
readOnly: boolean; readOnly: boolean;
toggleReadOnly: () => void; toggleReadOnly: () => void;
controller: ILibraryItemEditor;
} }
function AccessToolbar({ visible, toggleVisible, readOnly, toggleReadOnly }: AccessToolbarProps) { function AccessToolbar({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: AccessToolbarProps) {
const controller = useRSEdit();
const { accessLevel } = useAccessMode(); const { accessLevel } = useAccessMode();
const policy = useMemo( const policy = useMemo(
() => controller.schema?.access_policy ?? AccessPolicy.PRIVATE, () => controller.schema?.access_policy ?? AccessPolicy.PRIVATE,

View File

@ -10,21 +10,20 @@ import Tooltip from '@/components/ui/Tooltip';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useUsers } from '@/context/UsersContext'; import { useUsers } from '@/context/UsersContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { ILibraryItemEx } from '@/models/library'; import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
import { UserID, UserLevel } from '@/models/user'; import { UserID, UserLevel } from '@/models/user';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import LabeledValue from '../../../components/ui/LabeledValue'; import LabeledValue from '../../../components/ui/LabeledValue';
import { useRSEdit } from '../RSEditContext';
interface EditorLibraryItemProps { interface EditorLibraryItemProps {
item?: ILibraryItemEx; item?: ILibraryItemData;
isModified?: boolean; isModified?: boolean;
controller: ILibraryItemEditor;
} }
function EditorLibraryItem({ item, isModified }: EditorLibraryItemProps) { function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemProps) {
const { getUserLabel, users } = useUsers(); const { getUserLabel, users } = useUsers();
const controller = useRSEdit();
const { accessLevel } = useAccessMode(); const { accessLevel } = useAccessMode();
const intl = useIntl(); const intl = useIntl();

View File

@ -8,20 +8,22 @@ import { useAuth } from '@/context/AuthContext';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
import EditorLibraryItem from './EditorLibraryItem'; import EditorLibraryItem from './EditorLibraryItem';
import FormRSForm from './FormRSForm'; import FormRSForm from './FormRSForm';
import RSFormStats from './RSFormStats'; import RSFormStats from './RSFormStats';
import RSFormToolbar from './RSFormToolbar'; import RSFormToolbar from './RSFormToolbar';
interface EditorRSFormProps { interface EditorRSFormCardProps {
isModified: boolean; isModified: boolean;
setIsModified: React.Dispatch<React.SetStateAction<boolean>>; setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
onDestroy: () => void; onDestroy: () => void;
} }
function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProps) { function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSFormCardProps) {
const { schema, isSubscribed } = useRSForm(); const { schema, isSubscribed } = useRSForm();
const { user } = useAuth(); const { user } = useAuth();
const controller = useRSEdit();
function initiateSubmit() { function initiateSubmit() {
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement; const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
@ -51,7 +53,7 @@ function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProp
<AnimateFade onKeyDown={handleInput} className={clsx('sm:w-fit mx-auto', 'flex flex-col sm:flex-row')}> <AnimateFade onKeyDown={handleInput} className={clsx('sm:w-fit mx-auto', 'flex flex-col sm:flex-row')}>
<FlexColumn className='px-3'> <FlexColumn className='px-3'>
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} /> <FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
<EditorLibraryItem item={schema} isModified={isModified} /> <EditorLibraryItem item={schema} isModified={isModified} controller={controller} />
</FlexColumn> </FlexColumn>
<RSFormStats stats={schema?.stats} /> <RSFormStats stats={schema?.stats} />
@ -60,4 +62,4 @@ function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProp
); );
} }
export default EditorRSForm; export default EditorRSFormCard;

View File

@ -117,6 +117,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
toggleVisible={() => setVisible(prev => !prev)} toggleVisible={() => setVisible(prev => !prev)}
readOnly={readOnly} readOnly={readOnly}
toggleReadOnly={() => setReadOnly(prev => !prev)} toggleReadOnly={() => setReadOnly(prev => !prev)}
controller={controller}
/> />
<Label text='Версия' className='mb-2' /> <Label text='Версия' className='mb-2' />
<SelectVersion <SelectVersion

View File

@ -0,0 +1 @@
export { default } from './EditorRSFormCard';

View File

@ -182,7 +182,7 @@ export const RSEditState = ({
); );
const viewVersion = useCallback( const viewVersion = useCallback(
(version?: VersionID, newTab?: boolean) => router.push(urls.schema(model.schemaID, version), newTab), (version?: VersionID, newTab?: boolean) => router.push(urls.schema(model.itemID, version), newTab),
[router, model] [router, model]
); );
@ -721,7 +721,7 @@ export const RSEditState = ({
{model.loading ? <Loader /> : null} {model.loading ? <Loader /> : null}
{model.errorLoading ? ( {model.errorLoading ? (
<ProcessError error={model.errorLoading} isArchive={model.isArchive} schemaID={model.schemaID} /> <ProcessError error={model.errorLoading} isArchive={model.isArchive} itemID={model.itemID} />
) : null} ) : null}
{model.schema && !model.loading ? children : null} {model.schema && !model.loading ? children : null}
</RSEditContext.Provider> </RSEditContext.Provider>
@ -732,11 +732,11 @@ export const RSEditState = ({
function ProcessError({ function ProcessError({
error, error,
isArchive, isArchive,
schemaID itemID
}: { }: {
error: ErrorData; error: ErrorData;
isArchive: boolean; isArchive: boolean;
schemaID: string; itemID: string;
}): React.ReactElement { }): React.ReactElement {
if (axios.isAxiosError(error) && error.response && error.response.status === 404) { if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
return ( return (
@ -745,7 +745,7 @@ function ProcessError({
<div className='flex justify-center'> <div className='flex justify-center'>
<TextURL text='Библиотека' href='/library' /> <TextURL text='Библиотека' href='/library' />
{isArchive ? <Divider vertical margins='mx-3' /> : null} {isArchive ? <Divider vertical margins='mx-3' /> : null}
{isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${schemaID}`} /> : null} {isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${itemID}`} /> : null}
</div> </div>
</div> </div>
); );

View File

@ -14,7 +14,7 @@ function RSFormPage() {
const version = query.get('v') ?? undefined; const version = query.get('v') ?? undefined;
return ( return (
<AccessModeState> <AccessModeState>
<RSFormState schemaID={params.id ?? ''} versionID={version}> <RSFormState itemID={params.id ?? ''} versionID={version}>
<RSTabs /> <RSTabs />
</RSFormState> </RSFormState>
</AccessModeState> </AccessModeState>

View File

@ -18,7 +18,7 @@ import { PARAMETER, prefixes } from '@/utils/constants';
import { labelVersion } from '@/utils/labels'; import { labelVersion } from '@/utils/labels';
import EditorConstituenta from './EditorConstituenta'; import EditorConstituenta from './EditorConstituenta';
import EditorRSForm from './EditorRSForm'; import EditorRSForm from './EditorRSFormCard';
import EditorRSList from './EditorRSList'; import EditorRSList from './EditorRSList';
import EditorTermGraph from './EditorTermGraph'; import EditorTermGraph from './EditorTermGraph';
import { RSEditState } from './RSEditContext'; import { RSEditState } from './RSEditContext';