mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Add OSS scaffolding
This commit is contained in:
parent
d4d1c81bdc
commit
3f2db1e7bc
|
@ -6,6 +6,7 @@ import LibraryPage from '@/pages/LibraryPage';
|
|||
import LoginPage from '@/pages/LoginPage';
|
||||
import ManualsPage from '@/pages/ManualsPage';
|
||||
import NotFoundPage from '@/pages/NotFoundPage';
|
||||
import OssPage from '@/pages/OssPage';
|
||||
import PasswordChangePage from '@/pages/PasswordChangePage';
|
||||
import RegisterPage from '@/pages/RegisterPage';
|
||||
import RestorePasswordPage from '@/pages/RestorePasswordPage';
|
||||
|
@ -57,6 +58,10 @@ export const Router = createBrowserRouter([
|
|||
path: `${routes.rsforms}/:id`,
|
||||
element: <RSFormPage />
|
||||
},
|
||||
{
|
||||
path: `${routes.oss}/:id`,
|
||||
element: <OssPage />
|
||||
},
|
||||
{
|
||||
path: routes.manuals,
|
||||
element: <ManualsPage />
|
||||
|
|
|
@ -7,8 +7,17 @@ import { toast } from 'react-toastify';
|
|||
|
||||
import { type ErrorData } from '@/components/info/InfoError';
|
||||
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 { IOperationSchemaData } from '@/models/oss';
|
||||
import {
|
||||
IConstituentaList,
|
||||
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>) {
|
||||
if (!version) {
|
||||
AxiosGet({
|
||||
|
|
|
@ -17,7 +17,8 @@ export const routes = {
|
|||
create_schema: 'library/create',
|
||||
manuals: 'manuals',
|
||||
help: 'manuals',
|
||||
rsforms: 'rsforms'
|
||||
rsforms: 'rsforms',
|
||||
oss: 'oss'
|
||||
};
|
||||
|
||||
interface SchemaProps {
|
||||
|
@ -27,6 +28,11 @@ interface SchemaProps {
|
|||
active?: number | string;
|
||||
}
|
||||
|
||||
interface OssProps {
|
||||
id: number | string;
|
||||
tab: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal navigation URLs.
|
||||
*/
|
||||
|
@ -49,5 +55,8 @@ export const urls = {
|
|||
const versionStr = version !== undefined ? `v=${version}&` : '';
|
||||
const activeStr = active !== undefined ? `&active=${active}` : '';
|
||||
return `/rsforms/${id}?${versionStr}tab=${tab}${activeStr}`;
|
||||
},
|
||||
oss_props: ({ id, tab }: OssProps) => {
|
||||
return `/oss/${id}?tab=${tab}`;
|
||||
}
|
||||
};
|
||||
|
|
275
rsconcept/frontend/src/context/OssContext.tsx
Normal file
275
rsconcept/frontend/src/context/OssContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -54,7 +54,7 @@ import { useLibrary } from './LibraryContext';
|
|||
|
||||
interface IRSFormContext {
|
||||
schema?: IRSForm;
|
||||
schemaID: string;
|
||||
itemID: string;
|
||||
versionID?: string;
|
||||
|
||||
loading: boolean;
|
||||
|
@ -105,12 +105,12 @@ export const useRSForm = () => {
|
|||
};
|
||||
|
||||
interface RSFormStateProps {
|
||||
schemaID: string;
|
||||
itemID: string;
|
||||
versionID?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps) => {
|
||||
export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) => {
|
||||
const library = useLibrary();
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
|
@ -120,7 +120,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
setSchema,
|
||||
loading
|
||||
} = useRSFormDetails({
|
||||
target: schemaID,
|
||||
target: itemID,
|
||||
version: versionID
|
||||
});
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
@ -148,7 +148,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
patchLibraryItem(schemaID, {
|
||||
patchLibraryItem(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
|
@ -160,7 +160,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, setSchema, schema, library]
|
||||
[itemID, setSchema, schema, library]
|
||||
);
|
||||
|
||||
const upload = useCallback(
|
||||
|
@ -169,7 +169,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
patchUploadTRS(schemaID, {
|
||||
patchUploadTRS(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
|
@ -181,7 +181,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, setSchema, schema, library]
|
||||
[itemID, setSchema, schema, library]
|
||||
);
|
||||
|
||||
const subscribe = useCallback(
|
||||
|
@ -190,7 +190,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
postSubscribe(schemaID, {
|
||||
postSubscribe(itemID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setProcessingError,
|
||||
|
@ -206,7 +206,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, schema, user]
|
||||
[itemID, schema, user]
|
||||
);
|
||||
|
||||
const unsubscribe = useCallback(
|
||||
|
@ -215,7 +215,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
deleteUnsubscribe(schemaID, {
|
||||
deleteUnsubscribe(itemID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setProcessingError,
|
||||
|
@ -231,7 +231,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, schema, user]
|
||||
[itemID, schema, user]
|
||||
);
|
||||
|
||||
const setOwner = useCallback(
|
||||
|
@ -240,7 +240,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
patchSetOwner(schemaID, {
|
||||
patchSetOwner(itemID, {
|
||||
data: {
|
||||
user: newOwner
|
||||
},
|
||||
|
@ -254,7 +254,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, schema, library]
|
||||
[itemID, schema, library]
|
||||
);
|
||||
|
||||
const setAccessPolicy = useCallback(
|
||||
|
@ -263,7 +263,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
patchSetAccessPolicy(schemaID, {
|
||||
patchSetAccessPolicy(itemID, {
|
||||
data: {
|
||||
access_policy: newPolicy
|
||||
},
|
||||
|
@ -277,7 +277,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, schema, library]
|
||||
[itemID, schema, library]
|
||||
);
|
||||
|
||||
const setLocation = useCallback(
|
||||
|
@ -286,7 +286,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
patchSetLocation(schemaID, {
|
||||
patchSetLocation(itemID, {
|
||||
data: {
|
||||
location: newLocation
|
||||
},
|
||||
|
@ -300,7 +300,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, schema, library]
|
||||
[itemID, schema, library]
|
||||
);
|
||||
|
||||
const setEditors = useCallback(
|
||||
|
@ -309,7 +309,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
patchSetEditors(schemaID, {
|
||||
patchSetEditors(itemID, {
|
||||
data: {
|
||||
users: newEditors
|
||||
},
|
||||
|
@ -322,7 +322,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, schema]
|
||||
[itemID, schema]
|
||||
);
|
||||
|
||||
const resetAliases = useCallback(
|
||||
|
@ -331,7 +331,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
patchResetAliases(schemaID, {
|
||||
patchResetAliases(itemID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
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(
|
||||
|
@ -351,7 +351,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
return;
|
||||
}
|
||||
setProcessingError(undefined);
|
||||
patchRestoreOrder(schemaID, {
|
||||
patchRestoreOrder(itemID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
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(
|
||||
(data: ITargetCst, callback?: DataCallback<ConstituentaID[]>) => {
|
||||
setProcessingError(undefined);
|
||||
patchProduceStructure(schemaID, {
|
||||
patchProduceStructure(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
|
@ -380,26 +380,26 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[setSchema, library, schemaID]
|
||||
[setSchema, library, itemID]
|
||||
);
|
||||
|
||||
const download = useCallback(
|
||||
(callback: DataCallback<Blob>) => {
|
||||
setProcessingError(undefined);
|
||||
getTRSFile(schemaID, String(schema?.version ?? ''), {
|
||||
getTRSFile(itemID, String(schema?.version ?? ''), {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setProcessingError,
|
||||
onSuccess: callback
|
||||
});
|
||||
},
|
||||
[schemaID, schema]
|
||||
[itemID, schema]
|
||||
);
|
||||
|
||||
const cstCreate = useCallback(
|
||||
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||
setProcessingError(undefined);
|
||||
postNewConstituenta(schemaID, {
|
||||
postNewConstituenta(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
|
@ -411,13 +411,13 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, library, setSchema]
|
||||
[itemID, library, setSchema]
|
||||
);
|
||||
|
||||
const cstDelete = useCallback(
|
||||
(data: IConstituentaList, callback?: () => void) => {
|
||||
setProcessingError(undefined);
|
||||
patchDeleteConstituenta(schemaID, {
|
||||
patchDeleteConstituenta(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
|
@ -429,7 +429,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[schemaID, library, setSchema]
|
||||
[itemID, library, setSchema]
|
||||
);
|
||||
|
||||
const cstUpdate = useCallback(
|
||||
|
@ -442,18 +442,18 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
onError: setProcessingError,
|
||||
onSuccess: newData =>
|
||||
reload(setProcessing, () => {
|
||||
library.localUpdateTimestamp(Number(schemaID));
|
||||
library.localUpdateTimestamp(Number(itemID));
|
||||
if (callback) callback(newData);
|
||||
})
|
||||
});
|
||||
},
|
||||
[schemaID, library, reload]
|
||||
[itemID, library, reload]
|
||||
);
|
||||
|
||||
const cstRename = useCallback(
|
||||
(data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||
setProcessingError(undefined);
|
||||
patchRenameConstituenta(schemaID, {
|
||||
patchRenameConstituenta(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
|
@ -465,13 +465,13 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[setSchema, library, schemaID]
|
||||
[setSchema, library, itemID]
|
||||
);
|
||||
|
||||
const cstSubstitute = useCallback(
|
||||
(data: ICstSubstituteData, callback?: () => void) => {
|
||||
setProcessingError(undefined);
|
||||
patchSubstituteConstituents(schemaID, {
|
||||
patchSubstituteConstituents(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
|
@ -483,43 +483,43 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
}
|
||||
});
|
||||
},
|
||||
[setSchema, library, schemaID]
|
||||
[setSchema, library, itemID]
|
||||
);
|
||||
|
||||
const cstMoveTo = useCallback(
|
||||
(data: ICstMovetoData, callback?: () => void) => {
|
||||
setProcessingError(undefined);
|
||||
patchMoveConstituenta(schemaID, {
|
||||
patchMoveConstituenta(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setProcessingError,
|
||||
onSuccess: newData => {
|
||||
setSchema(newData);
|
||||
library.localUpdateTimestamp(Number(schemaID));
|
||||
library.localUpdateTimestamp(Number(itemID));
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
[schemaID, library, setSchema]
|
||||
[itemID, library, setSchema]
|
||||
);
|
||||
|
||||
const versionCreate = useCallback(
|
||||
(data: IVersionData, callback?: (version: number) => void) => {
|
||||
setProcessingError(undefined);
|
||||
postCreateVersion(schemaID, {
|
||||
postCreateVersion(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setProcessingError,
|
||||
onSuccess: newData => {
|
||||
setSchema(newData.schema);
|
||||
library.localUpdateTimestamp(Number(schemaID));
|
||||
library.localUpdateTimestamp(Number(itemID));
|
||||
if (callback) callback(newData.version);
|
||||
}
|
||||
});
|
||||
},
|
||||
[schemaID, library, setSchema]
|
||||
[itemID, library, setSchema]
|
||||
);
|
||||
|
||||
const versionUpdate = useCallback(
|
||||
|
@ -592,19 +592,19 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
onError: setProcessingError,
|
||||
onSuccess: newData => {
|
||||
setSchema(newData);
|
||||
library.localUpdateTimestamp(Number(schemaID));
|
||||
library.localUpdateTimestamp(Number(itemID));
|
||||
if (callback) callback(newData);
|
||||
}
|
||||
});
|
||||
},
|
||||
[library, schemaID, setSchema]
|
||||
[library, itemID, setSchema]
|
||||
);
|
||||
|
||||
return (
|
||||
<RSFormContext.Provider
|
||||
value={{
|
||||
schema,
|
||||
schemaID,
|
||||
itemID,
|
||||
versionID,
|
||||
loading,
|
||||
errorLoading,
|
||||
|
|
53
rsconcept/frontend/src/hooks/useOssDetails.ts
Normal file
53
rsconcept/frontend/src/hooks/useOssDetails.ts
Normal 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;
|
23
rsconcept/frontend/src/models/OssLoader.ts
Normal file
23
rsconcept/frontend/src/models/OssLoader.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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[];
|
||||
editors: UserID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents library item extended data with versions.
|
||||
*/
|
||||
export interface ILibraryItemVersioned extends ILibraryItemData {
|
||||
version?: VersionID;
|
||||
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}.
|
||||
*/
|
||||
|
|
23
rsconcept/frontend/src/models/oss.ts
Normal file
23
rsconcept/frontend/src/models/oss.ts
Normal 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
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import { Graph } from '@/models/Graph';
|
||||
|
||||
import { ILibraryItem, ILibraryItemEx, LibraryItemID } from './library';
|
||||
import { ILibraryItem, ILibraryItemVersioned, LibraryItemID } from './library';
|
||||
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang';
|
||||
|
||||
/**
|
||||
|
@ -226,7 +226,7 @@ export interface IRSFormStats {
|
|||
/**
|
||||
* Represents formal explication for set of concepts.
|
||||
*/
|
||||
export interface IRSForm extends ILibraryItemEx {
|
||||
export interface IRSForm extends ILibraryItemVersioned {
|
||||
items: IConstituenta[];
|
||||
stats: IRSFormStats;
|
||||
graph: Graph;
|
||||
|
@ -237,7 +237,7 @@ export interface IRSForm extends ILibraryItemEx {
|
|||
/**
|
||||
* Represents data for {@link IRSForm} provided by backend.
|
||||
*/
|
||||
export interface IRSFormData extends ILibraryItemEx {
|
||||
export interface IRSFormData extends ILibraryItemVersioned {
|
||||
items: IConstituentaData[];
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { useConceptOptions } from '@/context/OptionsContext';
|
|||
import { useUsers } from '@/context/UsersContext';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { ILibraryItem } from '@/models/library';
|
||||
import { ILibraryItem, LibraryItemType } from '@/models/library';
|
||||
import { storage } from '@/utils/constants';
|
||||
|
||||
interface ViewLibraryProps {
|
||||
|
@ -33,7 +33,11 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
|
|||
const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50);
|
||||
|
||||
function handleOpenItem(item: ILibraryItem, event: CProps.EventMouse) {
|
||||
router.push(urls.schema(item.id), event.ctrlKey || event.metaKey);
|
||||
if (item.item_type === LibraryItemType.RSFORM) {
|
||||
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();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
function HelpAccess() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Организация доступов к схемам</h1>
|
||||
<h1>Организация доступов</h1>
|
||||
<p>TBD.</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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;
|
140
rsconcept/frontend/src/pages/OssPage/EditorOssCard/FormOSS.tsx
Normal file
140
rsconcept/frontend/src/pages/OssPage/EditorOssCard/FormOSS.tsx
Normal 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;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EditorOssCard';
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EditorOssGraph';
|
198
rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx
Normal file
198
rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx
Normal 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} />;
|
||||
}
|
||||
}
|
21
rsconcept/frontend/src/pages/OssPage/OssPage.tsx
Normal file
21
rsconcept/frontend/src/pages/OssPage/OssPage.tsx
Normal 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;
|
143
rsconcept/frontend/src/pages/OssPage/OssTabs.tsx
Normal file
143
rsconcept/frontend/src/pages/OssPage/OssTabs.tsx
Normal 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;
|
203
rsconcept/frontend/src/pages/OssPage/OssTabsMenu.tsx
Normal file
203
rsconcept/frontend/src/pages/OssPage/OssTabsMenu.tsx
Normal 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;
|
1
rsconcept/frontend/src/pages/OssPage/index.tsx
Normal file
1
rsconcept/frontend/src/pages/OssPage/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './OssPage';
|
|
@ -1 +0,0 @@
|
|||
export { default } from './EditorRSForm';
|
|
@ -8,21 +8,19 @@ import Label from '@/components/ui/Label';
|
|||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
import { AccessPolicy } from '@/models/library';
|
||||
import { AccessPolicy, ILibraryItemEditor } from '@/models/library';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { UserLevel } from '@/models/user';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface AccessToolbarProps {
|
||||
visible: boolean;
|
||||
toggleVisible: () => void;
|
||||
readOnly: boolean;
|
||||
toggleReadOnly: () => void;
|
||||
controller: ILibraryItemEditor;
|
||||
}
|
||||
|
||||
function AccessToolbar({ visible, toggleVisible, readOnly, toggleReadOnly }: AccessToolbarProps) {
|
||||
const controller = useRSEdit();
|
||||
function AccessToolbar({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: AccessToolbarProps) {
|
||||
const { accessLevel } = useAccessMode();
|
||||
const policy = useMemo(
|
||||
() => controller.schema?.access_policy ?? AccessPolicy.PRIVATE,
|
|
@ -10,21 +10,20 @@ import Tooltip from '@/components/ui/Tooltip';
|
|||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
import { useUsers } from '@/context/UsersContext';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { ILibraryItemEx } from '@/models/library';
|
||||
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
|
||||
import { UserID, UserLevel } from '@/models/user';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import LabeledValue from '../../../components/ui/LabeledValue';
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface EditorLibraryItemProps {
|
||||
item?: ILibraryItemEx;
|
||||
item?: ILibraryItemData;
|
||||
isModified?: boolean;
|
||||
controller: ILibraryItemEditor;
|
||||
}
|
||||
|
||||
function EditorLibraryItem({ item, isModified }: EditorLibraryItemProps) {
|
||||
function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemProps) {
|
||||
const { getUserLabel, users } = useUsers();
|
||||
const controller = useRSEdit();
|
||||
const { accessLevel } = useAccessMode();
|
||||
const intl = useIntl();
|
||||
|
|
@ -8,20 +8,22 @@ import { useAuth } from '@/context/AuthContext';
|
|||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import EditorLibraryItem from './EditorLibraryItem';
|
||||
import FormRSForm from './FormRSForm';
|
||||
import RSFormStats from './RSFormStats';
|
||||
import RSFormToolbar from './RSFormToolbar';
|
||||
|
||||
interface EditorRSFormProps {
|
||||
interface EditorRSFormCardProps {
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onDestroy: () => void;
|
||||
}
|
||||
|
||||
function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProps) {
|
||||
function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSFormCardProps) {
|
||||
const { schema, isSubscribed } = useRSForm();
|
||||
const { user } = useAuth();
|
||||
const controller = useRSEdit();
|
||||
|
||||
function initiateSubmit() {
|
||||
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')}>
|
||||
<FlexColumn className='px-3'>
|
||||
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
||||
<EditorLibraryItem item={schema} isModified={isModified} />
|
||||
<EditorLibraryItem item={schema} isModified={isModified} controller={controller} />
|
||||
</FlexColumn>
|
||||
|
||||
<RSFormStats stats={schema?.stats} />
|
||||
|
@ -60,4 +62,4 @@ function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProp
|
|||
);
|
||||
}
|
||||
|
||||
export default EditorRSForm;
|
||||
export default EditorRSFormCard;
|
|
@ -117,6 +117,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
|||
toggleVisible={() => setVisible(prev => !prev)}
|
||||
readOnly={readOnly}
|
||||
toggleReadOnly={() => setReadOnly(prev => !prev)}
|
||||
controller={controller}
|
||||
/>
|
||||
<Label text='Версия' className='mb-2' />
|
||||
<SelectVersion
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EditorRSFormCard';
|
|
@ -182,7 +182,7 @@ export const RSEditState = ({
|
|||
);
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
|
@ -721,7 +721,7 @@ export const RSEditState = ({
|
|||
|
||||
{model.loading ? <Loader /> : null}
|
||||
{model.errorLoading ? (
|
||||
<ProcessError error={model.errorLoading} isArchive={model.isArchive} schemaID={model.schemaID} />
|
||||
<ProcessError error={model.errorLoading} isArchive={model.isArchive} itemID={model.itemID} />
|
||||
) : null}
|
||||
{model.schema && !model.loading ? children : null}
|
||||
</RSEditContext.Provider>
|
||||
|
@ -732,11 +732,11 @@ export const RSEditState = ({
|
|||
function ProcessError({
|
||||
error,
|
||||
isArchive,
|
||||
schemaID
|
||||
itemID
|
||||
}: {
|
||||
error: ErrorData;
|
||||
isArchive: boolean;
|
||||
schemaID: string;
|
||||
itemID: string;
|
||||
}): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
|
||||
return (
|
||||
|
@ -745,7 +745,7 @@ function ProcessError({
|
|||
<div className='flex justify-center'>
|
||||
<TextURL text='Библиотека' href='/library' />
|
||||
{isArchive ? <Divider vertical margins='mx-3' /> : null}
|
||||
{isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${schemaID}`} /> : null}
|
||||
{isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${itemID}`} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ function RSFormPage() {
|
|||
const version = query.get('v') ?? undefined;
|
||||
return (
|
||||
<AccessModeState>
|
||||
<RSFormState schemaID={params.id ?? ''} versionID={version}>
|
||||
<RSFormState itemID={params.id ?? ''} versionID={version}>
|
||||
<RSTabs />
|
||||
</RSFormState>
|
||||
</AccessModeState>
|
||||
|
|
|
@ -18,7 +18,7 @@ import { PARAMETER, prefixes } from '@/utils/constants';
|
|||
import { labelVersion } from '@/utils/labels';
|
||||
|
||||
import EditorConstituenta from './EditorConstituenta';
|
||||
import EditorRSForm from './EditorRSForm';
|
||||
import EditorRSForm from './EditorRSFormCard';
|
||||
import EditorRSList from './EditorRSList';
|
||||
import EditorTermGraph from './EditorTermGraph';
|
||||
import { RSEditState } from './RSEditContext';
|
||||
|
|
Loading…
Reference in New Issue
Block a user