F: Implement react-query pt4

This commit is contained in:
Ivan 2025-01-26 22:24:34 +03:00
parent 6543d88cbe
commit 519b5f6634
50 changed files with 1317 additions and 1588 deletions

View File

@ -1,63 +0,0 @@
'use client';
import { createContext, useCallback, useContext, useState } from 'react';
import { useOss, useOssInvalidate, useOssUpdate } from '@/backend/oss/useOSS';
import { ErrorData } from '@/components/info/InfoError';
import { LibraryItemID } from '@/models/library';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { contextOutsideScope } from '@/utils/labels';
interface IGlobalOssContext {
schema: IOperationSchema | undefined;
setID: (id: LibraryItemID | undefined) => void;
setData: (data: IOperationSchemaData) => void;
loading: boolean;
loadingError: ErrorData;
invalidate: () => Promise<void>;
invalidateItem: (target: LibraryItemID) => void;
partialUpdate: (data: Partial<IOperationSchema>) => void;
}
const GlobalOssContext = createContext<IGlobalOssContext | null>(null);
export const useGlobalOss = (): IGlobalOssContext => {
const context = useContext(GlobalOssContext);
if (context === null) {
throw new Error(contextOutsideScope('useGlobalOss', 'GlobalOssState'));
}
return context;
};
export const GlobalOssState = ({ children }: React.PropsWithChildren) => {
const [ossID, setID] = useState<LibraryItemID | undefined>(undefined);
const { schema: schema, error: loadingError, isLoading: loading } = useOss({ itemID: ossID });
const { update, partialUpdate } = useOssUpdate({ itemID: ossID });
const { invalidate } = useOssInvalidate({ itemID: ossID });
const invalidateItem = useCallback(
(target: LibraryItemID) => {
if (schema?.schemas.includes(target)) {
invalidate().catch(console.error);
}
},
[invalidate, schema]
);
return (
<GlobalOssContext
value={{
schema,
setID,
setData: update,
loading,
loadingError,
partialUpdate,
invalidateItem,
invalidate
}}
>
{children}
</GlobalOssContext>
);
};

View File

@ -6,7 +6,6 @@ import { ErrorBoundary } from 'react-error-boundary';
import { IntlProvider } from 'react-intl';
import { queryClient } from '@/backend/queryClient';
import { GlobalOssState } from '@/app/GlobalOssContext';
import ErrorFallback from './ErrorFallback';
@ -31,12 +30,10 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
>
<IntlProvider locale='ru' defaultLocale='ru'>
<QueryClientProvider client={queryClient}>
<GlobalOssState>
<ReactQueryDevtools initialIsOpen={false} />
{children}
</GlobalOssState>
</QueryClientProvider>
</IntlProvider>
</ErrorBoundary>);

View File

@ -2,10 +2,13 @@ import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, VersionID } from '@/models/library';
import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, LibraryItemType, VersionID } from '@/models/library';
import { ConstituentaID, IRSFormData } from '@/models/rsform';
import { UserID } from '@/models/user';
import { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
/**
* Represents update data for renaming Location.
*/
@ -67,6 +70,11 @@ export const libraryApi = {
})
.then(response => response.data)
}),
getItemQueryOptions: ({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) => {
return itemType === LibraryItemType.RSFORM
? rsformsApi.getRSFormQueryOptions({ itemID })
: ossApi.getOssQueryOptions({ itemID });
},
getTemplatesQueryOptions: () =>
queryOptions({
queryKey: [libraryApi.baseKey, 'templates'],

View File

@ -1,8 +1,12 @@
import { useIsMutating } from '@tanstack/react-query';
import { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
import { libraryApi } from './api';
export const useIsProcessingLibrary = () => {
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] });
return countMutations !== 0;
const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] });
const countRSForm = useIsMutating({ mutationKey: [rsformsApi.baseKey] });
return countMutations + countOss + countRSForm !== 0;
};

View File

@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { ILibraryItemVersioned, LibraryItemID, LibraryItemType } from '@/models/library';
import { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
export function useLibraryItem({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) {
const { data: rsForm } = useQuery({
...rsformsApi.getRSFormQueryOptions({ itemID }),
enabled: itemType === LibraryItemType.RSFORM
});
const { data: oss } = useQuery({
...ossApi.getOssQueryOptions({ itemID }),
enabled: itemType === LibraryItemType.OSS
});
return {
item:
itemType === LibraryItemType.RSFORM
? (rsForm as ILibraryItemVersioned | undefined)
: (oss as ILibraryItemVersioned | undefined)
};
}

View File

@ -92,7 +92,7 @@ export interface ICstRelocateDTO {
}
export const ossApi = {
baseKey: 'library',
baseKey: 'oss',
getOssQueryOptions: ({ itemID }: { itemID?: LibraryItemID }) => {
return queryOptions({

View File

@ -1,10 +1,10 @@
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
import { LibraryItemID } from '@/models/library';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { OssLoader } from '@/models/OssLoader';
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
import { ossApi } from './api';
export function useOss({ itemID }: { itemID?: LibraryItemID }) {
@ -17,12 +17,12 @@ export function useOss({ itemID }: { itemID?: LibraryItemID }) {
return { schema: schema, isLoading: isLoading || libraryLoading, error: error };
}
export function useOssSuspense({ itemID }: { itemID?: LibraryItemID }) {
export function useOssSuspense({ itemID }: { itemID: LibraryItemID }) {
const { items: libraryItems } = useLibrarySuspense();
const { data } = useSuspenseQuery({
...ossApi.getOssQueryOptions({ itemID })
});
const schema = data ? new OssLoader(data, libraryItems).produceOSS() : undefined;
const schema = new OssLoader(data!, libraryItems).produceOSS();
return { schema };
}

View File

@ -14,7 +14,7 @@ export const useUpdatePositions = () => {
onSuccess: (_, variables) => updateTimestamp(variables.itemID)
});
return {
cstDelete: (
updatePositions: (
data: {
itemID: LibraryItemID; //
positions: IOperationPosition[];

View File

@ -107,7 +107,7 @@ export interface ICheckConstituentaDTO {
}
export const rsformsApi = {
baseKey: 'library',
baseKey: 'rsform',
getRSFormQueryOptions: ({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) => {
return queryOptions({

View File

@ -15,11 +15,11 @@ export function useRSForm({ itemID, version }: { itemID?: LibraryItemID; version
return { schema, isLoading, error };
}
export function useRSFormSuspense({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) {
export function useRSFormSuspense({ itemID, version }: { itemID: LibraryItemID; version?: VersionID }) {
const { data } = useSuspenseQuery({
...rsformsApi.getRSFormQueryOptions({ itemID, version })
});
const schema = data ? new RSFormLoader(data).produceRSForm() : undefined;
const schema = new RSFormLoader(data!).produceRSForm();
return { schema };
}

View File

@ -4,6 +4,7 @@ import clsx from 'clsx';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { IRSFormCloneDTO } from '@/backend/library/api';
@ -18,7 +19,6 @@ import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI';
import { ConstituentaID } from '@/models/rsform';

View File

@ -1,34 +1,51 @@
'use client';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
import { useVersionDelete } from '@/backend/library/useVersionDelete';
import { useVersionUpdate } from '@/backend/library/useVersionUpdate';
import { IconReset, IconSave } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import { IVersionData, IVersionInfo, VersionID } from '@/models/library';
import { ILibraryItemVersioned, IVersionData, IVersionInfo, VersionID } from '@/models/library';
import { useDialogsStore } from '@/stores/dialogs';
import { information } from '@/utils/labels';
import TableVersions from './TableVersions';
export interface DlgEditVersionsProps {
versions: IVersionInfo[];
onDelete: (versionID: VersionID) => void;
onUpdate: (versionID: VersionID, data: IVersionData) => void;
item: ILibraryItemVersioned;
}
function DlgEditVersions() {
const { versions, onDelete, onUpdate } = useDialogsStore(state => state.props as DlgEditVersionsProps);
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined);
const processing = false; // TODO: fix processing hook and versions update
const { item } = useDialogsStore(state => state.props as DlgEditVersionsProps);
const router = useConceptNavigation();
const processing = useIsProcessingLibrary();
const { versionDelete } = useVersionDelete();
const { versionUpdate } = useVersionUpdate();
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined);
const [version, setVersion] = useState('');
const [description, setDescription] = useState('');
const isValid = selected && versions.every(ver => ver.id === selected.id || ver.version != version);
const isValid = selected && item.versions.every(ver => ver.id === selected.id || ver.version != version);
const isModified = selected && (selected.version != version || selected.description != description);
function handleDeleteVersion(versionID: VersionID) {
versionDelete({ itemID: item.id, versionID: versionID }, () => {
toast.success(information.versionDestroyed);
if (versionID === versionID) {
router.push(urls.schema(item.id));
}
});
}
function handleUpdate() {
if (!isModified || !selected || processing || !isValid) {
return;
@ -37,7 +54,14 @@ function DlgEditVersions() {
version: version,
description: description
};
onUpdate(selected.id, data);
versionUpdate(
{
itemID: item.id, //
versionID: selected.id,
data: data
},
() => toast.success(information.changesSaved)
);
}
function handleReset() {
@ -57,9 +81,9 @@ function DlgEditVersions() {
<Modal readonly header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
<TableVersions
processing={processing}
items={versions}
onDelete={onDelete}
onSelect={versionID => setSelected(versions.find(ver => ver.id === versionID))}
items={item.versions}
onDelete={handleDeleteVersion}
onSelect={versionID => setSelected(item.versions.find(ver => ver.id === versionID))}
selected={selected?.id}
/>

View File

@ -100,16 +100,9 @@ export interface ILibraryItemVersioned extends ILibraryItemData {
* Represents common {@link ILibraryItem} editor controller.
*/
export interface ILibraryItemEditor {
schema?: ILibraryItemData;
schema: ILibraryItemData;
deleteSchema: () => void;
isMutable: boolean;
isProcessing: boolean;
isAttachedToOSS: boolean;
setOwner: (newOwner: UserID) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void;
promptLocation: () => void;
share: () => void;
}

View File

@ -1,9 +1,9 @@
import { useEffect } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import Loader from '@/components/ui/Loader';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { PARAMETER } from '@/utils/constants';
function HomePage() {

View File

@ -8,7 +8,6 @@ import {
IconEdit2,
IconEditor,
IconMenu,
IconNewVersion,
IconOwner,
IconReader,
IconShare,
@ -54,9 +53,6 @@ function HelpRSMenu() {
<li>
<IconClone className='inline-icon icon-green' /> Клонировать создать копию схемы
</li>
<li>
<IconNewVersion size='1.25rem' className='inline-icon icon-green' /> Сохранить версию
</li>
<li>
<IconDownload className='inline-icon' /> Выгрузить сохранить в файле формата Экстеор
</li>

View File

@ -3,22 +3,19 @@
import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn';
import { LibraryItemType } from '@/models/library';
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard';
import { useModificationStore } from '@/stores/modification';
import { globals } from '@/utils/constants';
import { useOssEdit } from '../OssEditContext';
import FormOSS from './FormOSS';
import OssStats from './OssStats';
interface EditorOssCardProps {
isModified: boolean;
setIsModified: (newValue: boolean) => void;
onDestroy: () => void;
}
function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardProps) {
function EditorOssCard() {
const controller = useOssEdit();
const { isModified } = useModificationStore();
function initiateSubmit() {
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
@ -38,12 +35,7 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
return (
<>
<ToolbarRSFormCard
modified={isModified}
onSubmit={initiateSubmit}
onDestroy={onDestroy}
controller={controller}
/>
<ToolbarRSFormCard onSubmit={initiateSubmit} controller={controller} />
<div
onKeyDown={handleInput}
className={clsx(
@ -54,8 +46,8 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
)}
>
<FlexColumn className='px-3'>
<FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
<EditorLibraryItem item={controller.schema} isModified={isModified} controller={controller} />
<FormOSS id={globals.library_item_editor} />
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.OSS} controller={controller} />
</FlexColumn>
{controller.schema ? <OssStats stats={controller.schema.stats} /> : null}

View File

@ -6,25 +6,27 @@ import { toast } from 'react-toastify';
import { ILibraryUpdateDTO } from '@/backend/library/api';
import { useUpdateItem } from '@/backend/library/useUpdateItem';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
import { IconSave } from '@/components/Icons';
import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import { LibraryItemType } from '@/models/library';
import ToolbarItemAccess from '@/pages/RSFormPage/EditorRSFormCard/ToolbarItemAccess';
import { useModificationStore } from '@/stores/modification';
import { information } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext';
interface FormOSSProps {
id?: string;
isModified: boolean;
setIsModified: (newValue: boolean) => void;
}
function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
function FormOSS({ id }: FormOSSProps) {
const { updateItem: update } = useUpdateItem();
const controller = useOssEdit();
const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingOss();
const schema = controller.schema;
const [title, setTitle] = useState(schema?.title ?? '');
@ -125,14 +127,14 @@ function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
label='Описание'
rows={3}
value={comment}
disabled={!controller.isMutable || controller.isProcessing}
disabled={!controller.isMutable || isProcessing}
onChange={event => setComment(event.target.value)}
/>
{controller.isMutable || isModified ? (
<SubmitButton
text='Сохранить изменения'
className='self-center mt-4'
loading={controller.isProcessing}
loading={isProcessing}
disabled={!isModified}
icon={<IconSave size='1.25rem' />}
/>

View File

@ -4,15 +4,10 @@ import { ReactFlowProvider } from 'reactflow';
import OssFlow from './OssFlow';
interface EditorOssGraphProps {
isModified: boolean;
setIsModified: (newValue: boolean) => void;
}
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
function EditorOssGraph() {
return (
<ReactFlowProvider>
<OssFlow isModified={isModified} setIsModified={setIsModified} />
<OssFlow />
</ReactFlowProvider>
);
}

View File

@ -2,6 +2,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
import {
IconChild,
IconConnect,
@ -49,6 +50,8 @@ function NodeContextMenu({
onRelocateConstituents
}: NodeContextMenuProps) {
const controller = useOssEdit();
const isProcessing = useIsProcessingOss();
const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const readyForSynthesis = (() => {
@ -64,7 +67,7 @@ function NodeContextMenu({
return false;
}
const argumentOperations = argumentIDs.map(id => controller.schema!.operationByID.get(id)!);
const argumentOperations = argumentIDs.map(id => controller.schema.operationByID.get(id)!);
if (argumentOperations.some(item => item.result === null)) {
return false;
}
@ -82,7 +85,7 @@ function NodeContextMenu({
useEffect(() => setIsOpen(true), []);
const handleOpenSchema = () => {
controller.openOperationSchema(operation.id);
controller.navigateOperationSchema(operation.id);
};
const handleEditSchema = () => {
@ -126,7 +129,7 @@ function NodeContextMenu({
text='Редактировать'
title='Редактировать операцию'
icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={!controller.isMutable || controller.isProcessing}
disabled={!controller.isMutable || isProcessing}
onClick={handleEditOperation}
/>
@ -135,7 +138,7 @@ function NodeContextMenu({
text='Открыть схему'
titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')}
icon={<IconRSForm size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
disabled={isProcessing}
onClick={handleOpenSchema}
/>
) : null}
@ -144,7 +147,7 @@ function NodeContextMenu({
text='Создать схему'
title='Создать пустую схему для загрузки'
icon={<IconNewRSForm size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
disabled={isProcessing}
onClick={handleCreateSchema}
/>
) : null}
@ -153,7 +156,7 @@ function NodeContextMenu({
text={!operation.result ? 'Загрузить схему' : 'Изменить схему'}
title='Выбрать схему для загрузки'
icon={<IconConnect size='1rem' className='icon-primary' />}
disabled={controller.isProcessing}
disabled={isProcessing}
onClick={handleEditSchema}
/>
) : null}
@ -166,7 +169,7 @@ function NodeContextMenu({
: 'Необходимо предоставить все аргументы'
}
icon={<IconExecute size='1rem' className='icon-green' />}
disabled={controller.isProcessing || !readyForSynthesis}
disabled={isProcessing || !readyForSynthesis}
onClick={handleRunSynthesis}
/>
) : null}
@ -176,7 +179,7 @@ function NodeContextMenu({
text='Конституенты'
titleHtml='Перенос конституент</br>между схемами'
icon={<IconChild size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
disabled={isProcessing}
onClick={handleRelocateConstituents}
/>
) : null}
@ -184,7 +187,7 @@ function NodeContextMenu({
<DropdownButton
text='Удалить операцию'
icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={!controller.isMutable || controller.isProcessing || !controller.canDelete(operation.id)}
disabled={!controller.isMutable || isProcessing || !controller.canDelete(operation.id)}
onClick={handleDeleteOperation}
/>
</Dropdown>

View File

@ -16,15 +16,23 @@ import {
useReactFlow
} from 'reactflow';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useLibrary } from '@/backend/library/useLibrary';
import { useInputCreate } from '@/backend/oss/useInputCreate';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
import { useOperationExecute } from '@/backend/oss/useOperationExecute';
import { useUpdatePositions } from '@/backend/oss/useUpdatePositions';
import { CProps } from '@/components/props';
import Overlay from '@/components/ui/Overlay';
import { OssNode } from '@/models/miscellaneous';
import { OperationID } from '@/models/oss';
import { useMainHeight } from '@/stores/appLayout';
import { useModificationStore } from '@/stores/modification';
import { useOSSGraphStore } from '@/stores/ossGraph';
import { APP_COLORS } from '@/styling/color';
import { PARAMETER } from '@/utils/constants';
import { errors } from '@/utils/labels';
import { errors, information } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext';
import { OssNodeTypes } from './graph/OssNodeTypes';
@ -34,20 +42,24 @@ import ToolbarOssGraph from './ToolbarOssGraph';
const ZOOM_MAX = 2;
const ZOOM_MIN = 0.5;
interface OssFlowProps {
isModified: boolean;
setIsModified: (newValue: boolean) => void;
}
function OssFlow({ isModified, setIsModified }: OssFlowProps) {
function OssFlow() {
const mainHeight = useMainHeight();
const controller = useOssEdit();
const router = useConceptNavigation();
const { items: libraryItems } = useLibrary();
const flow = useReactFlow();
const { setIsModified } = useModificationStore();
const isProcessing = useIsProcessingOss();
const showGrid = useOSSGraphStore(state => state.showGrid);
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
const { inputCreate } = useInputCreate();
const { operationExecute } = useOperationExecute();
const { updatePositions } = useUpdatePositions();
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [toggleReset, setToggleReset] = useState(false);
@ -89,8 +101,8 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
type: edgeStraight ? 'straight' : 'simplebezier',
animated: edgeAnimate,
targetHandle:
controller.schema!.operationByID.get(argument.argument)!.position_x >
controller.schema!.operationByID.get(argument.operation)!.position_x
controller.schema.operationByID.get(argument.argument)!.position_x >
controller.schema.operationByID.get(argument.operation)!.position_x
? 'right'
: 'left'
}))
@ -117,7 +129,18 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
}
function handleSavePositions() {
controller.savePositions(getPositions(), () => setIsModified(false));
const positions = getPositions();
updatePositions({ itemID: controller.schema.id, positions: positions }, () => {
positions.forEach(item => {
const operation = controller.schema.operationByID.get(item.id);
if (operation) {
operation.position_x = item.position_x;
operation.position_y = item.position_y;
}
});
toast.success(information.changesSaved);
setIsModified(false);
});
}
function handleCreateOperation(inputs: OperationID[]) {
@ -149,8 +172,19 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
handleDeleteOperation(controller.selected[0]);
}
function handleCreateInput(target: OperationID) {
controller.createInput(target, getPositions());
function handleInputCreate(target: OperationID) {
const operation = controller.schema.operationByID.get(target);
if (!operation) {
return;
}
if (libraryItems.find(item => item.alias === operation.alias && item.location === controller.schema.location)) {
toast.error(errors.inputAlreadyExists);
return;
}
inputCreate({ itemID: controller.schema.id, data: { target: target, positions: getPositions() } }, new_schema => {
toast.success(information.newLibraryItem);
router.push(urls.schema(new_schema.id));
});
}
function handleEditSchema(target: OperationID) {
@ -161,15 +195,21 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
controller.promptEditOperation(target, getPositions());
}
function handleExecuteOperation(target: OperationID) {
controller.executeOperation(target, getPositions());
function handleOperationExecute(target: OperationID) {
operationExecute(
{
itemID: controller.schema.id, //
data: { target: target, positions: getPositions() }
},
() => toast.success(information.operationExecuted)
);
}
function handleExecuteSelected() {
if (controller.selected.length !== 1) {
return;
}
handleExecuteOperation(controller.selected[0]);
handleOperationExecute(controller.selected[0]);
}
function handleRelocateConstituents(target: OperationID) {
@ -237,14 +277,14 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
event.preventDefault();
event.stopPropagation();
if (node.data.operation.result) {
controller.openOperationSchema(Number(node.id));
controller.navigateOperationSchema(Number(node.id));
} else {
handleEditOperation(Number(node.id));
}
}
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (controller.isProcessing) {
if (isProcessing) {
return;
}
if (!controller.isMutable) {
@ -274,7 +314,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
<div tabIndex={-1} onKeyDown={handleKeyDown}>
<Overlay position='top-[1.9rem] pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
<ToolbarOssGraph
isModified={isModified}
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
onCreate={() => handleCreateOperation(controller.selected)}
onDelete={handleDeleteSelected}
@ -289,10 +328,10 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
<NodeContextMenu
onHide={handleContextMenuHide}
onDelete={handleDeleteOperation}
onCreateInput={handleCreateInput}
onCreateInput={handleInputCreate}
onEditSchema={handleEditSchema}
onEditOperation={handleEditOperation}
onExecuteOperation={handleExecuteOperation}
onExecuteOperation={handleOperationExecute}
onRelocateConstituents={handleRelocateConstituents}
{...menuProps}
/>

View File

@ -2,6 +2,7 @@
import clsx from 'clsx';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
import {
IconAnimation,
IconAnimationOff,
@ -21,6 +22,7 @@ import BadgeHelp from '@/components/info/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton';
import { HelpTopic } from '@/models/miscellaneous';
import { OperationType } from '@/models/oss';
import { useModificationStore } from '@/stores/modification';
import { useOSSGraphStore } from '@/stores/ossGraph';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/labels';
@ -28,7 +30,6 @@ import { prepareTooltip } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext';
interface ToolbarOssGraphProps {
isModified: boolean;
onCreate: () => void;
onDelete: () => void;
onEdit: () => void;
@ -40,7 +41,6 @@ interface ToolbarOssGraphProps {
}
function ToolbarOssGraph({
isModified,
onCreate,
onDelete,
onEdit,
@ -51,6 +51,8 @@ function ToolbarOssGraph({
onResetPositions
}: ToolbarOssGraphProps) {
const controller = useOssEdit();
const { isModified } = useModificationStore();
const isProcessing = useIsProcessingOss();
const selectedOperation = controller.schema?.operationByID.get(controller.selected[0]);
const showGrid = useOSSGraphStore(state => state.showGrid);
@ -73,7 +75,7 @@ function ToolbarOssGraph({
return false;
}
const argumentOperations = argumentIDs.map(id => controller.schema!.operationByID.get(id)!);
const argumentOperations = argumentIDs.map(id => controller.schema.operationByID.get(id)!);
if (argumentOperations.some(item => item.result === null)) {
return false;
}
@ -144,35 +146,31 @@ function ToolbarOssGraph({
<MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
icon={<IconSave size='1.25rem' className='icon-primary' />}
disabled={controller.isProcessing || !isModified}
disabled={isProcessing || !isModified}
onClick={onSavePositions}
/>
<MiniButton
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing}
disabled={isProcessing}
onClick={onCreate}
/>
<MiniButton
title='Активировать операцию'
icon={<IconExecute size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
disabled={isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
onClick={onExecute}
/>
<MiniButton
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
disabled={controller.selected.length !== 1 || controller.isProcessing}
disabled={controller.selected.length !== 1 || isProcessing}
onClick={onEdit}
/>
<MiniButton
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={
controller.selected.length !== 1 ||
controller.isProcessing ||
!controller.canDelete(controller.selected[0])
}
disabled={controller.selected.length !== 1 || isProcessing || !controller.canDelete(controller.selected[0])}
onClick={onDelete}
/>
</div>

View File

@ -3,6 +3,7 @@
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
import {
IconAdmin,
IconAlert,
@ -25,18 +26,17 @@ import useDropdown from '@/hooks/useDropdown';
import { UserRole } from '@/models/user';
import { useRoleStore } from '@/stores/role';
import { describeAccessMode as describeUserRole, labelAccessMode as labelUserRole } from '@/utils/labels';
import { sharePage } from '@/utils/utils';
import { useOssEdit } from './OssEditContext';
interface MenuOssTabsProps {
onDestroy: () => void;
}
function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
function MenuOssTabs() {
const controller = useOssEdit();
const router = useConceptNavigation();
const { user } = useAuth();
const isProcessing = useIsProcessingOss();
const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole);
@ -46,12 +46,12 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
function handleDelete() {
schemaMenu.hide();
onDestroy();
controller.deleteSchema();
}
function handleShare() {
schemaMenu.hide();
controller.share();
sharePage();
}
function handleChangeRole(newMode: UserRole) {
@ -96,7 +96,7 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
<DropdownButton
text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={controller.isProcessing || role < UserRole.OWNER}
disabled={isProcessing || role < UserRole.OWNER}
onClick={handleDelete}
/>
) : null}
@ -136,7 +136,7 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
text='Конституенты'
titleHtml='Перенос конституент</br>между схемами'
icon={<IconChild size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
disabled={isProcessing}
onClick={handleRelocate}
/>
</Dropdown>

View File

@ -6,24 +6,30 @@ import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useLibrary } from '@/backend/library/useLibrary';
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
import { useSetEditors } from '@/backend/library/useSetEditors';
import { useSetLocation } from '@/backend/library/useSetLocation';
import { useSetOwner } from '@/backend/library/useSetOwner';
import { useDeleteItem } from '@/backend/library/useDeleteItem';
import { useInputUpdate } from '@/backend/oss/useInputUpdate';
import { useOperationCreate } from '@/backend/oss/useOperationCreate';
import { useOperationDelete } from '@/backend/oss/useOperationDelete';
import { useOperationUpdate } from '@/backend/oss/useOperationUpdate';
import { useOssSuspense } from '@/backend/oss/useOSS';
import { AccessPolicy, ILibraryItemEditor, LibraryItemID } from '@/models/library';
import { Position2D } from '@/models/miscellaneous';
import { useRelocateConstituents } from '@/backend/oss/useRelocateConstituents';
import { useUpdatePositions } from '@/backend/oss/useUpdatePositions';
import { ILibraryItemEditor, LibraryItemID } from '@/models/library';
import { calculateInsertPosition } from '@/models/miscellaneousAPI';
import { IOperationPosition, IOperationSchema, OperationID, OperationType } from '@/models/oss';
import { UserID, UserRole } from '@/models/user';
import { UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences';
import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants';
import { errors, information } from '@/utils/labels';
import { information, prompts } from '@/utils/labels';
import { RSTabID } from '../RSFormPage/RSTabs';
import { RSTabID } from '../RSFormPage/RSEditContext';
export enum OssTabID {
CARD = 0,
GRAPH = 1
}
export interface ICreateOperationPrompt {
defaultX: number;
@ -34,36 +40,27 @@ export interface ICreateOperationPrompt {
}
export interface IOssEditContext extends ILibraryItemEditor {
schema?: IOperationSchema;
schema: IOperationSchema;
selected: OperationID[];
isOwned: boolean;
isMutable: boolean;
isProcessing: boolean;
isAttachedToOSS: boolean;
showTooltip: boolean;
setShowTooltip: (newValue: boolean) => void;
setOwner: (newOwner: UserID) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void;
promptLocation: () => void;
navigateTab: (tab: OssTabID) => void;
navigateOperationSchema: (target: OperationID) => void;
deleteSchema: () => void;
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
share: () => void;
openOperationSchema: (target: OperationID) => void;
savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
promptCreateOperation: (props: ICreateOperationPrompt) => void;
canDelete: (target: OperationID) => boolean;
promptCreateOperation: (props: ICreateOperationPrompt) => void;
promptDeleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
createInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
promptRelocateConstituents: (target: OperationID | undefined, positions: IOperationPosition[]) => void;
}
@ -78,325 +75,229 @@ export const useOssEdit = () => {
interface OssEditStateProps {
itemID: LibraryItemID;
selected: OperationID[];
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
}
export const OssEditState = ({
itemID,
selected,
setSelected,
children
}: React.PropsWithChildren<OssEditStateProps>) => {
export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEditStateProps>) => {
const router = useConceptNavigation();
const { user } = useAuth();
const { items: libraryItems } = useLibrary();
const adminMode = usePreferencesStore(state => state.adminMode);
const role = useRoleStore(state => state.role);
const adjustRole = useRoleStore(state => state.adjustRole);
const model = useOssSuspense({ itemID: itemID });
const { schema } = useOssSuspense({ itemID: itemID });
const { setOwner: setItemOwner } = useSetOwner();
const { setLocation: setItemLocation } = useSetLocation();
const { setAccessPolicy: setItemAccessPolicy } = useSetAccessPolicy();
const { setEditors: setItemEditors } = useSetEditors();
const isOwned = user?.id === schema.owner || false;
const isOwned = user?.id === model.schema?.owner || false;
const isMutable = role > UserRole.READER && !model.schema?.read_only;
const isMutable = role > UserRole.READER && !schema.read_only;
const [showTooltip, setShowTooltip] = useState(true);
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
const [createCallback, setCreateCallback] = useState<((newID: OperationID) => void) | undefined>(undefined);
const [selected, setSelected] = useState<OperationID[]>([]);
const showEditEditors = useDialogsStore(state => state.showEditEditors);
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
const showEditOperation = useDialogsStore(state => state.showEditOperation);
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
const [positions, setPositions] = useState<IOperationPosition[]>([]);
const { deleteItem } = useDeleteItem();
const { updatePositions } = useUpdatePositions();
const { operationCreate } = useOperationCreate();
const { operationDelete } = useOperationDelete();
const { operationUpdate } = useOperationUpdate();
const { relocateConstituents } = useRelocateConstituents();
const { inputUpdate } = useInputUpdate();
useEffect(
() =>
adjustRole({
isOwner: model.isOwned,
isEditor: (user && model.schema?.editors.includes(user?.id)) ?? false,
isOwner: isOwned,
isEditor: (user && schema.editors.includes(user?.id)) ?? false,
isStaff: user?.is_staff ?? false,
adminMode: adminMode
}),
[model.schema, adjustRole, model.isOwned, user, adminMode]
[schema, adjustRole, isOwned, user, adminMode]
);
const handleSetLocation = (newLocation: string) =>
setItemLocation({ itemID: model.itemID!, location: newLocation }, () => toast.success(information.moveComplete));
function navigateTab(tab: OssTabID) {
if (!schema) {
return;
}
const url = urls.oss_props({
id: schema.id,
tab: tab
});
router.push(url);
}
const share = useCallback(() => {
const currentRef = window.location.href;
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
navigator.clipboard
.writeText(url)
.then(() => toast.success(information.linkReady))
.catch(console.error);
}, []);
const setOwner = (newOwner: UserID) =>
setItemOwner({ itemID: model.itemID!, owner: newOwner }, () => toast.success(information.changesSaved));
const setAccessPolicy = (newPolicy: AccessPolicy) =>
setItemAccessPolicy({ itemID: model.itemID!, policy: newPolicy }, () => toast.success(information.changesSaved));
const handleSetEditors = (newEditors: UserID[]) =>
setItemEditors({ itemID: model.itemID!, editors: newEditors }, () => toast.success(information.changesSaved));
const openOperationSchema = useCallback(
const navigateOperationSchema = useCallback(
(target: OperationID) => {
const node = model.schema?.operationByID.get(target);
const node = schema.operationByID.get(target);
if (!node?.result) {
return;
}
router.push(urls.schema_props({ id: node.result, tab: RSTabID.CST_LIST }));
},
[router, model]
[router, schema]
);
const savePositions = useCallback(
(positions: IOperationPosition[], callback?: () => void) => {
model.savePositions({ positions: positions }, () => {
positions.forEach(item => {
const operation = model.schema?.operationByID.get(item.id);
if (operation) {
operation.position_x = item.position_x;
operation.position_y = item.position_y;
function deleteSchema() {
if (!schema || !window.confirm(prompts.deleteOSS)) {
return;
}
deleteItem(schema.id, () => {
toast.success(information.itemDestroyed);
router.push(urls.library);
});
}
function promptCreateOperation({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) {
showCreateOperation({
oss: schema,
onCreate: data => {
const target = calculateInsertPosition(schema, data.item_data.operation_type, data.arguments ?? [], positions, {
x: defaultX,
y: defaultY
});
data.positions = positions;
data.item_data.position_x = target.x;
data.item_data.position_y = target.y;
operationCreate({ itemID: schema.id, data }, operation => {
toast.success(information.newOperation(operation.alias));
if (callback) {
setTimeout(() => callback(operation.id), PARAMETER.refreshTimeout);
}
});
toast.success(information.changesSaved);
callback?.();
});
},
[model]
);
},
initialInputs: inputs
});
}
const handleCreateOperation = useCallback(
(data: IOperationCreateData) => {
const target = calculateInsertPosition(
model.schema!,
data.item_data.operation_type,
data.arguments,
positions,
insertPosition
);
data.positions = positions;
data.item_data.position_x = target.x;
data.item_data.position_y = target.y;
model.createOperation(data, operation => {
toast.success(information.newOperation(operation.alias));
if (createCallback) {
setTimeout(() => createCallback(operation.id), PARAMETER.refreshTimeout);
}
});
},
[model, positions, insertPosition, createCallback]
);
function canDelete(target: OperationID) {
const operation = schema.operationByID.get(target);
if (!operation) {
return false;
}
if (operation.operation_type === OperationType.INPUT) {
return true;
}
return schema.graph.expandOutputs([target]).length === 0;
}
const handleEditOperation = useCallback(
(data: IOperationUpdateData) => {
data.positions = positions;
model.updateOperation(data, () => toast.success(information.changesSaved));
},
[model, positions]
);
const canDelete = useCallback(
(target: OperationID) => {
if (!model.schema) {
return false;
function promptEditOperation(target: OperationID, positions: IOperationPosition[]) {
const operation = schema.operationByID.get(target);
if (!operation) {
return;
}
showEditOperation({
oss: schema,
target: operation,
onSubmit: data => {
data.positions = positions;
operationUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
}
const operation = model.schema.operationByID.get(target);
if (!operation) {
return false;
}
if (operation.operation_type === OperationType.INPUT) {
return true;
}
return model.schema.graph.expandOutputs([target]).length === 0;
},
[model]
);
});
}
const handleDeleteOperation = useCallback(
(targetID: OperationID, keepConstituents: boolean, deleteSchema: boolean) => {
const data: IOperationDeleteData = {
target: targetID,
positions: positions,
keep_constituents: keepConstituents,
delete_schema: deleteSchema
};
model.deleteOperation(data, () => toast.success(information.operationDestroyed));
},
[model, positions]
);
const createInput = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
const operation = model.schema?.operationByID.get(target);
if (!model.schema || !operation) {
return;
}
if (libraryItems.find(item => item.alias === operation.alias && item.location === model.schema!.location)) {
toast.error(errors.inputAlreadyExists);
return;
}
model.createInput({ target: target, positions: positions }, new_schema => {
toast.success(information.newLibraryItem);
router.push(urls.schema(new_schema.id));
});
},
[model, libraryItems, router]
);
const setTargetInput = useCallback(
(target: OperationID, newInput: LibraryItemID | undefined) => {
const data: IOperationSetInputData = {
target: target,
positions: positions,
input: newInput ?? null
};
model.setInput(data, () => toast.success(information.changesSaved));
},
[model, positions]
);
const handleRelocateConstituents = useCallback(
(data: ICstRelocateData) => {
if (
positions.every(item => {
const operation = model.schema!.operationByID.get(item.id)!;
return operation.position_x === item.position_x && operation.position_y === item.position_y;
})
) {
model.relocateConstituents(data, () => toast.success(information.changesSaved));
} else {
model.savePositions({ positions: positions }, () =>
model.relocateConstituents(data, () => toast.success(information.changesSaved))
function promptDeleteOperation(target: OperationID, positions: IOperationPosition[]) {
const operation = schema.operationByID.get(target);
if (!operation) {
return;
}
showDeleteOperation({
target: operation,
onSubmit: (targetID, keepConstituents, deleteSchema) => {
operationDelete(
{
itemID: schema.id,
data: {
target: targetID,
positions: positions,
keep_constituents: keepConstituents,
delete_schema: deleteSchema
}
},
() => toast.success(information.operationDestroyed)
);
}
},
[model, positions]
);
});
}
const executeOperation = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
const data = {
target: target,
positions: positions
};
model.executeOperation(data, () => toast.success(information.operationExecuted));
},
[model]
);
const promptEditors = () => showEditEditors({ editors: model.schema?.editors ?? [], setEditors: handleSetEditors });
const promptLocation = () =>
showEditLocation({ initial: model.schema?.location ?? '', onChangeLocation: handleSetLocation });
const promptCreateOperation = useCallback(
({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) => {
if (!model.schema) {
return;
function promptEditInput(target: OperationID, positions: IOperationPosition[]) {
const operation = schema.operationByID.get(target);
if (!operation) {
return;
}
showEditInput({
oss: schema,
target: operation,
onSubmit: (target, newInput) => {
inputUpdate(
{
itemID: schema.id,
data: {
target: target,
positions: positions,
input: newInput ?? null
}
},
() => toast.success(information.changesSaved)
);
}
setInsertPosition({ x: defaultX, y: defaultY });
setPositions(positions);
setCreateCallback(() => callback);
showCreateOperation({ oss: model.schema, onCreate: handleCreateOperation, initialInputs: inputs });
},
[model.schema, showCreateOperation, handleCreateOperation]
);
});
}
const promptEditOperation = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
const operation = model.schema?.operationByID.get(target);
if (!model.schema || !operation) {
return;
function promptRelocateConstituents(target: OperationID | undefined, positions: IOperationPosition[]) {
const operation = target ? schema.operationByID.get(target) : undefined;
showRelocateConstituents({
oss: schema,
initialTarget: operation,
onSubmit: data => {
if (
positions.every(item => {
const operation = schema.operationByID.get(item.id)!;
return operation.position_x === item.position_x && operation.position_y === item.position_y;
})
) {
relocateConstituents({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
} else {
updatePositions(
{
itemID: schema.id, //
positions: positions
},
() => relocateConstituents({ itemID: schema.id, data }, () => toast.success(information.changesSaved))
);
}
}
setPositions(positions);
showEditOperation({ oss: model.schema, target: operation, onSubmit: handleEditOperation });
},
[model.schema, showEditOperation, handleEditOperation]
);
const promptDeleteOperation = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
const operation = model.schema?.operationByID.get(target);
if (!model.schema || !operation) {
return;
}
setPositions(positions);
showDeleteOperation({ target: operation, onSubmit: handleDeleteOperation });
},
[model.schema, showDeleteOperation, handleDeleteOperation]
);
const promptEditInput = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
const operation = model.schema?.operationByID.get(target);
if (!model.schema || !operation) {
return;
}
setPositions(positions);
showEditInput({ oss: model.schema, target: operation, onSubmit: setTargetInput });
},
[model.schema, showEditInput, setTargetInput]
);
const promptRelocateConstituents = useCallback(
(target: OperationID | undefined, positions: IOperationPosition[]) => {
if (!model.schema) {
return;
}
const operation = target ? model.schema?.operationByID.get(target) : undefined;
setPositions(positions);
showRelocateConstituents({ oss: model.schema, initialTarget: operation, onSubmit: handleRelocateConstituents });
},
[model.schema, showRelocateConstituents, handleRelocateConstituents]
);
});
}
return (
<OssEditContext
value={{
schema: model.schema,
schema,
selected,
navigateTab,
deleteSchema,
showTooltip,
setShowTooltip,
isOwned,
isMutable,
isProcessing: model.processing,
isAttachedToOSS: false,
setOwner,
setAccessPolicy,
promptEditors,
promptLocation,
share,
setSelected,
openOperationSchema,
savePositions,
navigateOperationSchema,
promptCreateOperation,
canDelete,
promptDeleteOperation,
createInput,
promptEditInput,
promptEditOperation,
executeOperation,
promptRelocateConstituents
}}
>

View File

@ -1,23 +1,69 @@
'use client';
import { Suspense } from 'react';
import axios from 'axios';
import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router';
import Loader from '@/components/ui/Loader';
import { OssState } from '@/pages/OssPage/OssContext';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import TextURL from '@/components/ui/TextURL';
import { useModificationStore } from '@/stores/modification';
import { OssEditState } from './OssEditContext';
import OssTabs from './OssTabs';
function OssPage() {
const router = useConceptNavigation();
const params = useParams();
const itemID = params.id ? Number(params.id) : undefined;
const { isModified } = useModificationStore();
useBlockNavigation(isModified);
// useBlockNavigation(
// isModified &&
// schema !== undefined &&
// !!user &&
// (user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
// );
if (!itemID) {
router.replace(urls.page404);
return null;
}
return (
<Suspense fallback={<Loader />}>
<OssState itemID={itemID}>
<ErrorBoundary FallbackComponent={ProcessError}>
<OssEditState itemID={itemID}>
<OssTabs />
</OssState>
</Suspense>
</OssEditState>
</ErrorBoundary>
);
}
export default OssPage;
// ====== Internals =========
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 404) {
return (
<div className='flex flex-col items-center p-2 mx-auto'>
<p>{`Операционная схема с указанным идентификатором отсутствует`}</p>
<div className='flex justify-center'>
<TextURL text='Библиотека' href='/library' />
</div>
</div>
);
} else if (error.response.status === 403) {
return (
<div className='flex flex-col items-center p-2 mx-auto'>
<p>Владелец ограничил доступ к данной схеме</p>
<TextURL text='Библиотека' href='/library' />
</div>
);
}
}
return <InfoError error={error} />;
}

View File

@ -1,79 +1,46 @@
'use client';
import axios from 'axios';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useDeleteItem } from '@/backend/library/useDeleteItem';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import Loader from '@/components/ui/Loader';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL';
import useQueryStrings from '@/hooks/useQueryStrings';
import { OperationID } from '@/models/oss';
import { useAppLayoutStore } from '@/stores/appLayout';
import { information, prompts } from '@/utils/labels';
import { useModificationStore } from '@/stores/modification';
import EditorRSForm from './EditorOssCard';
import EditorTermGraph from './EditorOssGraph';
import MenuOssTabs from './MenuOssTabs';
import { OssEditState } from './OssEditContext';
export enum OssTabID {
CARD = 0,
GRAPH = 1
}
import { OssTabID, useOssEdit } from './OssEditContext';
function OssTabs() {
const router = useConceptNavigation();
const query = useQueryStrings();
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
const { user } = useAuth();
const { deleteItem } = useDeleteItem();
const { schema, navigateTab } = useOssEdit();
const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { schema, loading, loadingError: errorLoading } = useOSSControl();
const [isModified, setIsModified] = useState(false);
const [selected, setSelected] = useState<OperationID[]>([]);
useBlockNavigation(
isModified &&
schema !== undefined &&
!!user &&
(user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
);
const { setIsModified } = useModificationStore();
useEffect(() => setIsModified(false), [setIsModified]);
useEffect(() => {
if (schema) {
const oldTitle = document.title;
document.title = schema.title;
return () => {
document.title = oldTitle;
};
}
}, [schema, schema?.title]);
const oldTitle = document.title;
document.title = schema.title;
return () => {
document.title = oldTitle;
};
}, [schema.title]);
useEffect(() => {
hideFooter(activeTab === OssTabID.GRAPH);
}, [activeTab, hideFooter]);
function navigateTab(tab: OssTabID) {
if (!schema) {
return;
}
const url = urls.oss_props({
id: schema.id,
tab: tab
});
router.push(url);
}
function onSelectTab(index: number, last: number, event: Event) {
if (last === index) {
return;
@ -93,78 +60,34 @@ function OssTabs() {
navigateTab(index);
}
function onDestroySchema() {
if (!schema || !window.confirm(prompts.deleteOSS)) {
return;
}
deleteItem(schema.id, () => {
toast.success(information.itemDestroyed);
router.push(urls.library);
});
}
return (
<OssEditState selected={selected} setSelected={setSelected}>
{loading ? <Loader /> : null}
{errorLoading ? <ProcessError error={errorLoading} /> : null}
{schema && !loading ? (
<Tabs
selectedIndex={activeTab}
onSelect={onSelectTab}
defaultFocus
selectedTabClassName='clr-selected'
className='flex flex-col mx-auto min-w-fit'
>
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
<TabList className={clsx('w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}>
<MenuOssTabs onDestroy={onDestroySchema} />
<Tabs
selectedIndex={activeTab}
onSelect={onSelectTab}
defaultFocus
selectedTabClassName='clr-selected'
className='flex flex-col mx-auto min-w-fit'
>
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
<TabList className={clsx('w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}>
<MenuOssTabs />
<TabLabel label='Карточка' title={schema.title ?? ''} />
<TabLabel label='Граф' />
</TabList>
</Overlay>
<TabLabel label='Карточка' title={schema.title ?? ''} />
<TabLabel label='Граф' />
</TabList>
</Overlay>
<div className='overflow-x-hidden'>
<TabPanel>
<EditorRSForm
isModified={isModified} //
setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
<div className='overflow-x-hidden'>
<TabPanel>
<EditorRSForm />
</TabPanel>
<TabPanel>
<EditorTermGraph isModified={isModified} setIsModified={setIsModified} />
</TabPanel>
</div>
</Tabs>
) : null}
</OssEditState>
<TabPanel>
<EditorTermGraph />
</TabPanel>
</div>
</Tabs>
);
}
export default OssTabs;
// ====== Internals =========
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 404) {
return (
<div className='flex flex-col items-center p-2 mx-auto'>
<p>{`Операционная схема с указанным идентификатором отсутствует`}</p>
<div className='flex justify-center'>
<TextURL text='Библиотека' href='/library' />
</div>
</div>
);
} else if (error.response.status === 403) {
return (
<div className='flex flex-col items-center p-2 mx-auto'>
<p>Владелец ограничил доступ к данной схеме</p>
<TextURL text='Библиотека' href='/library' />
</div>
);
}
}
return <InfoError error={error} />;
}

View File

@ -1,55 +0,0 @@
import clsx from 'clsx';
import { IconEdit } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import { IConstituenta } from '@/models/rsform';
import { tooltips } from '@/utils/labels';
interface ControlsOverlayProps {
constituenta: IConstituenta;
disabled: boolean;
modified: boolean;
processing: boolean;
onRename: () => void;
onEditTerm: () => void;
}
function ControlsOverlay({ constituenta, disabled, modified, processing, onRename, onEditTerm }: ControlsOverlayProps) {
return (
<Overlay position='top-1 left-[4.7rem]' className='flex select-none'>
{!disabled || processing ? (
<MiniButton
title={modified ? tooltips.unsaved : `Редактировать словоформы термина`}
noHover
onClick={onEditTerm}
icon={<IconEdit size='1rem' className='icon-primary' />}
disabled={modified}
/>
) : null}
<div
className={clsx(
'pt-1 sm:pl-[1.375rem] pl-1', //
'text-sm font-medium whitespace-nowrap',
'select-text cursor-default',
disabled && !processing && 'pl-[1.6rem] sm:pl-[2.8rem]'
)}
>
<span>Имя </span>
<span className='ml-1'>{constituenta?.alias ?? ''}</span>
</div>
{!disabled || processing ? (
<MiniButton
noHover
title={modified ? tooltips.unsaved : 'Переименовать конституенту'}
onClick={onRename}
icon={<IconEdit size='1rem' className='icon-primary' />}
disabled={modified}
/>
) : null}
</Overlay>
);
}
export default ControlsOverlay;

View File

@ -2,12 +2,18 @@
import clsx from 'clsx';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta } from '@/models/rsform';
import { useMainHeight } from '@/stores/appLayout';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
import { globals } from '@/utils/constants';
import { information } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
import { useRSEdit } from '../RSEditContext';
import ViewConstituents from '../ViewConstituents';
@ -17,23 +23,20 @@ import ToolbarConstituenta from './ToolbarConstituenta';
// Threshold window width to switch layout.
const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
interface EditorConstituentaProps {
activeCst?: IConstituenta;
isModified: boolean;
setIsModified: (newValue: boolean) => void;
onOpenEdit: (cstID: ConstituentaID) => void;
}
function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }: EditorConstituentaProps) {
function EditorConstituenta() {
const controller = useRSEdit();
const windowSize = useWindowSize();
const mainHeight = useMainHeight();
const showList = usePreferencesStore(state => state.showCstSideList);
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
const { cstUpdate } = useCstUpdate();
const { isModified } = useModificationStore();
const [toggleReset, setToggleReset] = useState(false);
const disabled = !activeCst || !controller.isContentEditable || controller.isProcessing;
const isProcessing = useIsProcessingRSForm();
const disabled = !controller.activeCst || !controller.isContentEditable || isProcessing;
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
@ -56,6 +59,29 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
}
}
function handleEditTermForms() {
if (!controller.activeCst) {
return;
}
if (isModified && !promptUnsaved()) {
return;
}
showEditTerm({
target: controller.activeCst,
onSave: forms =>
cstUpdate(
{
itemID: controller.schema.id,
data: {
target: controller.activeCst!.id,
item_data: { term_forms: forms }
}
},
() => toast.success(information.changesSaved)
)
});
}
function initiateSubmit() {
const element = document.getElementById(globals.constituenta_editor) as HTMLFormElement;
if (element) {
@ -76,9 +102,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
return (
<>
<ToolbarConstituenta
activeCst={activeCst}
activeCst={controller.activeCst}
disabled={disabled}
modified={isModified}
onSubmit={initiateSubmit}
onReset={() => setToggleReset(prev => !prev)}
/>
@ -97,21 +122,13 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
<FormConstituenta
disabled={disabled}
id={globals.constituenta_editor}
state={activeCst}
isModified={isModified}
toggleReset={toggleReset}
setIsModified={setIsModified}
onEditTerm={controller.editTermForms}
onRename={controller.renameCst}
onOpenEdit={onOpenEdit}
onEditTerm={handleEditTermForms}
/>
<ViewConstituents
isMounted={showList}
schema={controller.schema}
expression={activeCst?.definition_formal ?? ''}
expression={controller.activeCst?.definition_formal ?? ''}
isBottom={isNarrow}
activeCst={activeCst}
onOpenEdit={onOpenEdit}
/>
</div>
</>

View File

@ -0,0 +1,84 @@
import clsx from 'clsx';
import { toast } from 'react-toastify';
import { ICstRenameDTO } from '@/backend/rsform/api';
import { useCstRename } from '@/backend/rsform/useCstRename';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { IconEdit } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import { IConstituenta } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { information, tooltips } from '@/utils/labels';
import { useRSEdit } from '../RSEditContext';
interface EditorControlsProps {
constituenta: IConstituenta;
disabled: boolean;
onEditTerm: () => void;
}
function EditorControls({ constituenta, disabled, onEditTerm }: EditorControlsProps) {
const { schema } = useRSEdit();
const { isModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const showRenameCst = useDialogsStore(state => state.showRenameCst);
const { cstRename } = useCstRename();
function handleRenameCst() {
const initialData: ICstRenameDTO = {
target: constituenta.id,
alias: constituenta.alias,
cst_type: constituenta.cst_type
};
showRenameCst({
schema: schema,
initial: initialData,
allowChangeType: !constituenta.is_inherited,
onRename: data => {
const oldAlias = initialData.alias;
cstRename({ itemID: schema.id, data }, () => toast.success(information.renameComplete(oldAlias, data.alias)));
}
});
}
return (
<Overlay position='top-1 left-[4.7rem]' className='flex select-none'>
{!disabled || isProcessing ? (
<MiniButton
title={isModified ? tooltips.unsaved : `Редактировать словоформы термина`}
noHover
onClick={onEditTerm}
icon={<IconEdit size='1rem' className='icon-primary' />}
disabled={isModified}
/>
) : null}
<div
className={clsx(
'pt-1 sm:pl-[1.375rem] pl-1', //
'text-sm font-medium whitespace-nowrap',
'select-text cursor-default',
disabled && !isProcessing && 'pl-[1.6rem] sm:pl-[2.8rem]'
)}
>
<span>Имя </span>
<span className='ml-1'>{constituenta?.alias ?? ''}</span>
</div>
{!disabled || isProcessing ? (
<MiniButton
noHover
title={isModified ? tooltips.unsaved : 'Переименовать конституенту'}
onClick={handleRenameCst}
icon={<IconEdit size='1rem' className='icon-primary' />}
disabled={isModified}
/>
) : null}
</Overlay>
);
}
export default EditorControls;

View File

@ -6,6 +6,7 @@ import { toast } from 'react-toastify';
import { ICstUpdateDTO } from '@/backend/rsform/api';
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
import { CProps } from '@/components/props';
import RefsInput from '@/components/RefsInput';
@ -13,27 +14,22 @@ import Indicator from '@/components/ui/Indicator';
import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea';
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
import { ConstituentaID, CstType } from '@/models/rsform';
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { errors, information, labelCstTypification } from '@/utils/labels';
import EditorRSExpression from '../EditorRSExpression';
import { useRSEdit } from '../RSEditContext';
import ControlsOverlay from './ControlsOverlay';
import EditorControls from './EditorControls';
interface FormConstituentaProps {
disabled: boolean;
id?: string;
state?: IConstituenta;
isModified: boolean;
disabled: boolean;
toggleReset: boolean;
setIsModified: (newValue: boolean) => void;
onRename: () => void;
onEditTerm: () => void;
onOpenEdit?: (cstID: ConstituentaID) => void;
}
@ -41,72 +37,68 @@ interface FormConstituentaProps {
function FormConstituenta({
disabled,
id,
state,
isModified,
setIsModified,
toggleReset,
onRename,
onEditTerm,
onOpenEdit
}: FormConstituentaProps) {
const { cstUpdate } = useCstUpdate();
const controller = useRSEdit();
const schema = controller.schema;
const { schema, activeCst } = useRSEdit();
const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const [term, setTerm] = useState(state?.term_raw ?? '');
const [textDefinition, setTextDefinition] = useState(state?.definition_raw ?? '');
const [expression, setExpression] = useState(state?.definition_formal ?? '');
const [convention, setConvention] = useState(state?.convention ?? '');
const [term, setTerm] = useState(activeCst?.term_raw ?? '');
const [textDefinition, setTextDefinition] = useState(activeCst?.definition_raw ?? '');
const [expression, setExpression] = useState(activeCst?.definition_formal ?? '');
const [convention, setConvention] = useState(activeCst?.convention ?? '');
const [typification, setTypification] = useState('N/A');
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
const typeInfo = state
const typeInfo = activeCst
? {
alias: state.alias,
result: localParse ? localParse.typification : state.parse.typification,
args: localParse ? localParse.args : state.parse.args
alias: activeCst.alias,
result: localParse ? localParse.typification : activeCst.parse.typification,
args: localParse ? localParse.args : activeCst.parse.args
}
: undefined;
const [forceComment, setForceComment] = useState(false);
const isBasic = !!state && isBasicConcept(state.cst_type);
const isElementary = !!state && isBaseSet(state.cst_type);
const showConvention = !state || !!state.convention || forceComment || isBasic;
const isBasic = !!activeCst && isBasicConcept(activeCst.cst_type);
const isElementary = !!activeCst && isBaseSet(activeCst.cst_type);
const showConvention = !activeCst || !!activeCst.convention || forceComment || isBasic;
const showTypification = useDialogsStore(state => state.showShowTypeGraph);
const showTypification = useDialogsStore(activeCst => activeCst.showShowTypeGraph);
useEffect(() => {
if (state) {
setConvention(state.convention);
setTerm(state.term_raw);
setTextDefinition(state.definition_raw);
setExpression(state.definition_formal);
setTypification(state ? labelCstTypification(state) : 'N/A');
if (activeCst) {
setConvention(activeCst.convention);
setTerm(activeCst.term_raw);
setTextDefinition(activeCst.definition_raw);
setExpression(activeCst.definition_formal);
setTypification(activeCst ? labelCstTypification(activeCst) : 'N/A');
setForceComment(false);
setLocalParse(undefined);
}
}, [state, schema, toggleReset, setIsModified]);
}, [activeCst, schema, toggleReset, setIsModified]);
useLayoutEffect(() => {
if (!state) {
if (!activeCst) {
setIsModified(false);
return;
}
setIsModified(
state.term_raw !== term ||
state.definition_raw !== textDefinition ||
state.convention !== convention ||
state.definition_formal !== expression
activeCst.term_raw !== term ||
activeCst.definition_raw !== textDefinition ||
activeCst.convention !== convention ||
activeCst.definition_formal !== expression
);
return () => setIsModified(false);
}, [
state,
state?.term_raw,
state?.definition_formal,
state?.definition_raw,
state?.convention,
activeCst,
activeCst?.term_raw,
activeCst?.definition_formal,
activeCst?.definition_raw,
activeCst?.convention,
term,
textDefinition,
expression,
@ -118,23 +110,23 @@ function FormConstituenta({
if (event) {
event.preventDefault();
}
if (!state || controller.isProcessing || !schema) {
if (!activeCst || isProcessing || !schema) {
return;
}
const data: ICstUpdateDTO = {
target: state.id,
target: activeCst.id,
item_data: {
term_raw: state.term_raw !== term ? term : undefined,
definition_formal: state.definition_formal !== expression ? expression : undefined,
definition_raw: state.definition_raw !== textDefinition ? textDefinition : undefined,
convention: state.convention !== convention ? convention : undefined
term_raw: activeCst.term_raw !== term ? term : undefined,
definition_formal: activeCst.definition_formal !== expression ? expression : undefined,
definition_raw: activeCst.definition_raw !== textDefinition ? textDefinition : undefined,
convention: activeCst.convention !== convention ? convention : undefined
}
};
cstUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
}
function handleTypeGraph(event: CProps.EventMouse) {
if (!state || (localParse && !localParse.parseResult) || state.parse.status !== ParsingStatus.VERIFIED) {
if (!activeCst || (localParse && !localParse.parseResult) || activeCst.parse.status !== ParsingStatus.VERIFIED) {
toast.error(errors.typeStructureFailed);
return;
}
@ -145,16 +137,7 @@ function FormConstituenta({
return (
<div className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'>
{state ? (
<ControlsOverlay
disabled={disabled}
modified={isModified}
processing={controller.isProcessing}
constituenta={state}
onEditTerm={onEditTerm}
onRename={onRename}
/>
) : null}
{activeCst ? <EditorControls disabled={disabled} constituenta={activeCst} onEditTerm={onEditTerm} /> : null}
<form id={id} className={clsx('cc-column', 'mt-1 md:w-[48.8rem] shrink-0', 'px-6 py-1')} onSubmit={handleSubmit}>
<RefsInput
key='cst_term'
@ -165,12 +148,12 @@ function FormConstituenta({
schema={schema}
onOpenEdit={onOpenEdit}
value={term}
initialValue={state?.term_raw ?? ''}
resolved={state?.term_resolved ?? 'Конституента не выбрана'}
initialValue={activeCst?.term_raw ?? ''}
resolved={activeCst?.term_resolved ?? 'Конституента не выбрана'}
disabled={disabled}
onChange={newValue => setTerm(newValue)}
/>
{state ? (
{activeCst ? (
<TextArea
id='cst_typification'
fitContent
@ -184,24 +167,26 @@ function FormConstituenta({
colors='bg-transparent clr-text-default cursor-default'
/>
) : null}
{state ? (
{activeCst ? (
<>
{!!state.definition_formal || !isElementary ? (
{!!activeCst.definition_formal || !isElementary ? (
<EditorRSExpression
id='cst_expression'
label={
state.cst_type === CstType.STRUCTURED
activeCst.cst_type === CstType.STRUCTURED
? 'Область определения'
: isFunctional(state.cst_type)
: isFunctional(activeCst.cst_type)
? 'Определение функции'
: 'Формальное определение'
}
placeholder={
state.cst_type !== CstType.STRUCTURED ? 'Родоструктурное выражение' : 'Типизация родовой структуры'
activeCst.cst_type !== CstType.STRUCTURED
? 'Родоструктурное выражение'
: 'Типизация родовой структуры'
}
value={expression}
activeCst={state}
disabled={disabled || state.is_inherited}
activeCst={activeCst}
disabled={disabled || activeCst.is_inherited}
toggleReset={toggleReset}
onChangeExpression={newValue => setExpression(newValue)}
onChangeTypification={setTypification}
@ -210,7 +195,7 @@ function FormConstituenta({
onShowTypeGraph={handleTypeGraph}
/>
) : null}
{!!state.definition_raw || !isElementary ? (
{!!activeCst.definition_raw || !isElementary ? (
<RefsInput
id='cst_definition'
label='Текстовое определение'
@ -220,8 +205,8 @@ function FormConstituenta({
schema={schema}
onOpenEdit={onOpenEdit}
value={textDefinition}
initialValue={state.definition_raw}
resolved={state.definition_resolved}
initialValue={activeCst.definition_raw}
resolved={activeCst.definition_resolved}
disabled={disabled}
onChange={newValue => setTextDefinition(newValue)}
/>
@ -236,12 +221,12 @@ function FormConstituenta({
label={isBasic ? 'Конвенция' : 'Комментарий'}
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
value={convention}
disabled={disabled || (isBasic && state.is_inherited)}
disabled={disabled || (isBasic && activeCst.is_inherited)}
onChange={event => setConvention(event.target.value)}
/>
) : null}
{!showConvention && (!disabled || controller.isProcessing) ? (
{!showConvention && (!disabled || isProcessing) ? (
<button
key='cst_disable_comment'
id='cst_disable_comment'
@ -254,7 +239,7 @@ function FormConstituenta({
</button>
) : null}
{!disabled || controller.isProcessing ? (
{!disabled || isProcessing ? (
<div className='mx-auto flex'>
<SubmitButton
key='cst_form_submit'
@ -264,13 +249,13 @@ function FormConstituenta({
icon={<IconSave size='1.25rem' />}
/>
<Overlay position='top-[0.1rem] left-[0.4rem]' className='cc-icons'>
{state.has_inherited_children && !state.is_inherited ? (
{activeCst.has_inherited_children && !activeCst.is_inherited ? (
<Indicator
icon={<IconPredecessor size='1.25rem' className='text-sec-600' />}
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза'
/>
) : null}
{state.is_inherited ? (
{activeCst.is_inherited ? (
<Indicator
icon={<IconChild size='1.25rem' className='text-sec-600' />}
titleHtml='Внимание!</br> Конституента является наследником<br/>'

View File

@ -2,6 +2,10 @@
import clsx from 'clsx';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useFindPredecessor } from '@/backend/oss/useFindPredecessor';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import {
IconClone,
IconDestroy,
@ -19,17 +23,17 @@ import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import { HelpTopic } from '@/models/miscellaneous';
import { IConstituenta } from '@/models/rsform';
import { ConstituentaID, IConstituenta } from '@/models/rsform';
import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip, tooltips } from '@/utils/labels';
import { useRSEdit } from '../RSEditContext';
import { RSTabID, useRSEdit } from '../RSEditContext';
interface ToolbarConstituentaProps {
activeCst?: IConstituenta;
disabled: boolean;
modified: boolean;
onSubmit: () => void;
onReset: () => void;
@ -38,15 +42,30 @@ interface ToolbarConstituentaProps {
function ToolbarConstituenta({
activeCst,
disabled,
modified,
onSubmit,
onReset
}: ToolbarConstituentaProps) {
const controller = useRSEdit();
const router = useConceptNavigation();
const { findPredecessor } = useFindPredecessor();
const showList = usePreferencesStore(state => state.showCstSideList);
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
const { isModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
function viewPredecessor(target: ConstituentaID) {
findPredecessor({ target: target }, reference =>
router.push(
urls.schema_props({
id: reference.schema,
active: reference.id,
tab: RSTabID.CST_EDIT
})
)
);
}
return (
<Overlay
@ -56,13 +75,13 @@ function ToolbarConstituenta({
{controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS
items={controller.schema.oss}
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
/>
) : null}
{activeCst?.is_inherited ? (
<MiniButton
title='Перейти к исходной конституенте в ОСС'
onClick={() => controller.viewPredecessor(activeCst.id)}
onClick={() => viewPredecessor(activeCst.id)}
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
/>
) : null}
@ -71,25 +90,25 @@ function ToolbarConstituenta({
<MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
icon={<IconSave size='1.25rem' className='icon-primary' />}
disabled={disabled || !modified}
disabled={disabled || !isModified}
onClick={onSubmit}
/>
<MiniButton
title='Сбросить несохраненные изменения'
icon={<IconReset size='1.25rem' className='icon-primary' />}
disabled={disabled || !modified}
disabled={disabled || !isModified}
onClick={onReset}
/>
<MiniButton
title='Создать конституенту после данной'
icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={!controller.isContentEditable || controller.isProcessing}
disabled={!controller.isContentEditable || isProcessing}
onClick={() => controller.createCst(activeCst?.cst_type, false)}
/>
<MiniButton
titleHtml={modified ? tooltips.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')}
titleHtml={isModified ? tooltips.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')}
icon={<IconClone size='1.25rem' className='icon-green' />}
disabled={disabled || modified}
disabled={disabled || isModified}
onClick={controller.cloneCst}
/>
<MiniButton
@ -112,13 +131,13 @@ function ToolbarConstituenta({
<MiniButton
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
disabled={disabled || modified || (controller.schema && controller.schema?.items.length < 2)}
disabled={disabled || isModified || (controller.schema && controller.schema?.items.length < 2)}
onClick={controller.moveUp}
/>
<MiniButton
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
disabled={disabled || modified || (controller.schema && controller.schema?.items.length < 2)}
disabled={disabled || isModified || (controller.schema && controller.schema?.items.length < 2)}
onClick={controller.moveDown}
/>
</>

View File

@ -4,6 +4,7 @@ import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import BadgeHelp from '@/components/info/BadgeHelp';
import { CProps } from '@/components/props';
import RSInput from '@/components/RSInput';
@ -62,6 +63,7 @@ function EditorRSExpression({
const parser = useRSParse({ schema: controller.schema });
const rsInput = useRef<ReactCodeMirrorRef>(null);
const isProcessing = useIsProcessingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls);
const showAST = useDialogsStore(state => state.showShowAST);
@ -173,7 +175,7 @@ function EditorRSExpression({
/>
<RSEditorControls
isOpen={showControls && (!disabled || (controller.isProcessing && !activeCst.is_inherited))}
isOpen={showControls && (!disabled || (isProcessing && !activeCst.is_inherited))}
disabled={disabled}
onEdit={handleEdit}
/>

View File

@ -1,8 +1,14 @@
import { Suspense, useCallback } from 'react';
import { Suspense } from 'react';
import { useIntl } from 'react-intl';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
import { useLibraryItem } from '@/backend/library/useLibraryItem';
import { useSetEditors } from '@/backend/library/useSetEditors';
import { useSetLocation } from '@/backend/library/useSetLocation';
import { useSetOwner } from '@/backend/library/useSetOwner';
import { useLabelUser } from '@/backend/users/useLabelUser';
import {
IconDateCreate,
@ -21,51 +27,80 @@ import Overlay from '@/components/ui/Overlay';
import Tooltip from '@/components/ui/Tooltip';
import ValueIcon from '@/components/ui/ValueIcon';
import useDropdown from '@/hooks/useDropdown';
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
import { ILibraryItemEditor, LibraryItemID, LibraryItemType } from '@/models/library';
import { UserID, UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs';
import { useLibrarySearchStore } from '@/stores/librarySearch';
import { useModificationStore } from '@/stores/modification';
import { useRoleStore } from '@/stores/role';
import { prefixes } from '@/utils/constants';
import { prompts } from '@/utils/labels';
import { information, prompts } from '@/utils/labels';
interface EditorLibraryItemProps {
item?: ILibraryItemData;
isModified?: boolean;
itemID: LibraryItemID;
itemType: LibraryItemType;
controller: ILibraryItemEditor;
}
function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemProps) {
function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemProps) {
const getUserLabel = useLabelUser();
const role = useRoleStore(state => state.role);
const intl = useIntl();
const router = useConceptNavigation();
const setLocation = useLibrarySearchStore(state => state.setLocation);
const setGlobalLocation = useLibrarySearchStore(state => state.setLocation);
const { item } = useLibraryItem({ itemID, itemType });
const isProcessing = useIsProcessingLibrary();
const { isModified } = useModificationStore();
const { setOwner } = useSetOwner();
const { setLocation } = useSetLocation();
const { setEditors } = useSetEditors();
const showEditEditors = useDialogsStore(state => state.showEditEditors);
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
const ownerSelector = useDropdown();
const onSelectUser = useCallback(
(newValue: UserID) => {
ownerSelector.hide();
if (newValue === item?.owner) {
return;
}
if (!window.confirm(prompts.ownerChange)) {
return;
}
controller.setOwner(newValue);
},
[controller, item?.owner, ownerSelector]
);
const onSelectUser = function (newValue: UserID) {
ownerSelector.hide();
if (newValue === item?.owner) {
return;
}
if (!window.confirm(prompts.ownerChange)) {
return;
}
setOwner({ itemID: itemID, owner: newValue }, () => toast.success(information.changesSaved));
};
const handleOpenLibrary = useCallback(
(event: CProps.EventMouse) => {
if (!item) {
return;
}
setLocation(item.location);
router.push(urls.library, event.ctrlKey || event.metaKey);
},
[setLocation, item, router]
);
function handleOpenLibrary(event: CProps.EventMouse) {
if (!item) {
return;
}
setGlobalLocation(item.location);
router.push(urls.library, event.ctrlKey || event.metaKey);
}
function handleEditLocation() {
if (!item) {
return;
}
showEditLocation({
initial: item.location,
onChangeLocation: newLocation =>
setLocation({ itemID: itemID, location: newLocation }, () => toast.success(information.moveComplete))
});
}
function handleEditEditors() {
if (!item) {
return;
}
showEditEditors({
editors: item.editors,
setEditors: newEditors =>
setEditors({ itemID: itemID, editors: newEditors }, () => toast.success(information.changesSaved))
});
}
if (!item) {
return null;
@ -86,8 +121,8 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
value={item.location}
title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'}
onClick={controller.promptLocation}
disabled={isModified || controller.isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
onClick={handleEditLocation}
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
/>
</div>
@ -108,7 +143,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
value={getUserLabel(item.owner)}
title={controller.isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'}
onClick={ownerSelector.toggle}
disabled={isModified || controller.isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
/>
<div className='sm:mb-1 flex justify-between items-center'>
@ -117,8 +152,8 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
dense
icon={<IconEditor size='1.25rem' className='icon-primary' />}
value={item.editors.length}
onClick={controller.promptEditors}
disabled={isModified || controller.isProcessing || role < UserRole.OWNER}
onClick={handleEditEditors}
disabled={isModified || isProcessing || role < UserRole.OWNER}
/>
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
<Suspense fallback={<Loader scale={2} />}>

View File

@ -3,6 +3,8 @@
import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn';
import { LibraryItemType } from '@/models/library';
import { useModificationStore } from '@/stores/modification';
import { globals } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
@ -11,14 +13,9 @@ import FormRSForm from './FormRSForm';
import RSFormStats from './RSFormStats';
import ToolbarRSFormCard from './ToolbarRSFormCard';
interface EditorRSFormCardProps {
isModified: boolean;
setIsModified: (newValue: boolean) => void;
onDestroy: () => void;
}
function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSFormCardProps) {
function EditorRSFormCard() {
const controller = useRSEdit();
const { isModified } = useModificationStore();
function initiateSubmit() {
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
@ -38,12 +35,7 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
return (
<>
<ToolbarRSFormCard
modified={isModified}
onSubmit={initiateSubmit}
onDestroy={onDestroy}
controller={controller}
/>
<ToolbarRSFormCard onSubmit={initiateSubmit} controller={controller} />
<div
onKeyDown={handleInput}
className={clsx(
@ -53,8 +45,8 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
)}
>
<FlexColumn className='flex-shrink'>
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
<EditorLibraryItem item={controller.schema} isModified={isModified} controller={controller} />
<FormRSForm id={globals.library_item_editor} />
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.RSFORM} controller={controller} />
</FlexColumn>
{controller.schema ? <RSFormStats stats={controller.schema.stats} isArchive={controller.isArchive} /> : null}

View File

@ -3,15 +3,19 @@
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { ILibraryUpdateDTO } from '@/backend/library/api';
import { useUpdateItem } from '@/backend/library/useUpdateItem';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { IconSave } from '@/components/Icons';
import SelectVersion from '@/components/select/SelectVersion';
import Label from '@/components/ui/Label';
import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import { LibraryItemType } from '@/models/library';
import { LibraryItemType, VersionID } from '@/models/library';
import { useModificationStore } from '@/stores/modification';
import { useRSEdit } from '../RSEditContext';
import ToolbarItemAccess from './ToolbarItemAccess';
@ -19,14 +23,15 @@ import ToolbarVersioning from './ToolbarVersioning';
interface FormRSFormProps {
id?: string;
isModified: boolean;
setIsModified: (newValue: boolean) => void;
}
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
function FormRSForm({ id }: FormRSFormProps) {
const controller = useRSEdit();
const router = useConceptNavigation();
const schema = controller.schema;
const { updateItem: update } = useUpdateItem();
const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const [title, setTitle] = useState(schema?.title ?? '');
const [alias, setAlias] = useState(schema?.alias ?? '');
@ -34,6 +39,10 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
const [visible, setVisible] = useState(schema?.visible ?? false);
const [readOnly, setReadOnly] = useState(schema?.read_only ?? false);
function handleSelectVersion(version?: VersionID) {
router.push(urls.schema(schema.id, version));
}
useEffect(() => {
if (schema) {
setTitle(schema.title);
@ -127,7 +136,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
className='select-none'
value={schema?.version} //
items={schema?.versions}
onSelectValue={controller.viewVersion}
onSelectValue={handleSelectVersion}
/>
</div>
</div>
@ -137,14 +146,14 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
label='Описание'
rows={3}
value={comment}
disabled={!controller.isContentEditable || controller.isProcessing}
disabled={!controller.isContentEditable || isProcessing}
onChange={event => setComment(event.target.value)}
/>
{controller.isContentEditable || isModified ? (
<SubmitButton
text='Сохранить изменения'
className='self-center mt-4'
loading={controller.isProcessing}
loading={isProcessing}
disabled={!isModified}
icon={<IconSave size='1.25rem' />}
/>

View File

@ -1,3 +1,7 @@
import { toast } from 'react-toastify';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
import { VisibilityIcon } from '@/components/DomainIcons';
import { IconImmutable, IconMutable } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp';
@ -10,6 +14,7 @@ import { HelpTopic } from '@/models/miscellaneous';
import { UserRole } from '@/models/user';
import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants';
import { information } from '@/utils/labels';
interface ToolbarItemAccessProps {
visible: boolean;
@ -21,23 +26,29 @@ interface ToolbarItemAccessProps {
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) {
const role = useRoleStore(state => state.role);
const policy = controller.schema?.access_policy ?? AccessPolicy.PRIVATE;
const isProcessing = useIsProcessingLibrary();
const policy = controller.schema.access_policy;
const { setAccessPolicy } = useSetAccessPolicy();
function handleSetAccessPolicy(newPolicy: AccessPolicy) {
setAccessPolicy({ itemID: controller.schema.id, policy: newPolicy }, () => toast.success(information.changesSaved));
}
return (
<Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'>
<Label text='Доступ' className='self-center select-none' />
<div className='ml-auto cc-icons'>
<SelectAccessPolicy
disabled={role <= UserRole.EDITOR || controller.isProcessing || controller.isAttachedToOSS}
disabled={role <= UserRole.EDITOR || isProcessing || controller.isAttachedToOSS}
value={policy}
onChange={newPolicy => controller.setAccessPolicy(newPolicy)}
onChange={handleSetAccessPolicy}
/>
<MiniButton
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
icon={<VisibilityIcon value={visible} />}
onClick={toggleVisible}
disabled={role === UserRole.READER || controller.isProcessing}
disabled={role === UserRole.READER || isProcessing}
/>
<MiniButton
@ -50,7 +61,7 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
)
}
onClick={toggleReadOnly}
disabled={role === UserRole.READER || controller.isProcessing}
disabled={role === UserRole.READER || isProcessing}
/>
<BadgeHelp topic={HelpTopic.ACCESS} className={PARAMETER.TOOLTIP_WIDTH} offset={4} />

View File

@ -1,5 +1,6 @@
'use client';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
@ -9,22 +10,24 @@ import { AccessPolicy, ILibraryItemEditor, LibraryItemType } from '@/models/libr
import { HelpTopic } from '@/models/miscellaneous';
import { IRSForm } from '@/models/rsform';
import { UserRole } from '@/models/user';
import { useModificationStore } from '@/stores/modification';
import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip, tooltips } from '@/utils/labels';
import { sharePage } from '@/utils/utils';
import { IRSEditContext } from '../RSEditContext';
interface ToolbarRSFormCardProps {
modified: boolean;
onSubmit: () => void;
onDestroy: () => void;
controller: ILibraryItemEditor;
}
function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: ToolbarRSFormCardProps) {
function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardProps) {
const role = useRoleStore(state => state.role);
const canSave = modified && !controller.isProcessing;
const { isModified } = useModificationStore();
const isProcessing = useIsProcessingLibrary();
const canSave = isModified && !isProcessing;
const ossSelector = (() => {
if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) {
@ -37,7 +40,9 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
return (
<MiniSelectorOSS
items={schema.oss}
onSelect={(event, value) => (controller as IRSEditContext).viewOSS(value.id, event.ctrlKey || event.metaKey)}
onSelect={(event, value) =>
(controller as IRSEditContext).navigateOss(value.id, event.ctrlKey || event.metaKey)
}
/>
);
})();
@ -45,7 +50,7 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
return (
<Overlay position='cc-tab-tools' className='cc-icons'>
{ossSelector}
{controller.isMutable || modified ? (
{controller.isMutable || isModified ? (
<MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
disabled={!canSave}
@ -56,15 +61,15 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
<MiniButton
titleHtml={tooltips.shareItem(controller.schema?.access_policy)}
icon={<IconShare size='1.25rem' className='icon-primary' />}
onClick={controller.share}
onClick={sharePage}
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
/>
{controller.isMutable ? (
<MiniButton
title='Удалить схему'
icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={!controller.isMutable || controller.isProcessing || role < UserRole.OWNER}
onClick={onDestroy}
disabled={!controller.isMutable || isProcessing || role < UserRole.OWNER}
onClick={controller.deleteSchema}
/>
) : null}
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} className={PARAMETER.TOOLTIP_WIDTH} />

View File

@ -1,9 +1,19 @@
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useVersionCreate } from '@/backend/library/useVersionCreate';
import { useVersionRestore } from '@/backend/library/useVersionRestore';
import { IconNewVersion, IconUpload, IconVersions } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import { HelpTopic } from '@/models/miscellaneous';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { PARAMETER } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
import { useRSEdit } from '../RSEditContext';
@ -13,6 +23,45 @@ interface ToolbarVersioningProps {
function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
const controller = useRSEdit();
const router = useConceptNavigation();
const { isModified } = useModificationStore();
const { versionRestore } = useVersionRestore();
const { versionCreate } = useVersionCreate();
const showCreateVersion = useDialogsStore(state => state.showCreateVersion);
const showEditVersions = useDialogsStore(state => state.showEditVersions);
function handleRestoreVersion() {
if (!controller.schema.version || !window.confirm(prompts.restoreArchive)) {
return;
}
versionRestore({ itemID: controller.schema.id, versionID: controller.schema.version }, () => {
toast.success(information.versionRestored);
router.push(urls.schema(controller.schema.id));
});
}
function handleCreateVersion() {
if (isModified && !promptUnsaved()) {
return;
}
showCreateVersion({
versions: controller.schema.versions,
selected: controller.selected,
totalCount: controller.schema.items.length,
onCreate: data =>
versionCreate({ itemID: controller.schema.id, data: data }, () => {
toast.success(information.newVersion(data.version));
})
});
}
function handleEditVersions() {
showEditVersions({
item: controller.schema
});
}
return (
<Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons' layer='z-bottom'>
{controller.isMutable ? (
@ -26,19 +75,19 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
: 'Переключитесь на <br/>неактуальную версию'
}
disabled={controller.isContentEditable || blockReload}
onClick={() => controller.restoreVersion()}
onClick={handleRestoreVersion}
icon={<IconUpload size='1.25rem' className='icon-red' />}
/>
<MiniButton
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
disabled={!controller.isContentEditable}
onClick={controller.createVersion}
onClick={handleCreateVersion}
icon={<IconNewVersion size='1.25rem' className='icon-green' />}
/>
<MiniButton
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
disabled={!controller.schema || controller.schema?.versions.length === 0}
onClick={controller.promptEditVersions}
onClick={handleEditVersions}
icon={<IconVersions size='1.25rem' className='icon-primary' />}
/>
</>

View File

@ -4,6 +4,7 @@ import fileDownload from 'js-file-download';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { IconCSV } from '@/components/Icons';
import { type RowSelectionState } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton';
@ -20,13 +21,10 @@ import { useRSEdit } from '../RSEditContext';
import TableRSList from './TableRSList';
import ToolbarRSList from './ToolbarRSList';
interface EditorRSListProps {
onOpenEdit: (cstID: ConstituentaID) => void;
}
function EditorRSList({ onOpenEdit }: EditorRSListProps) {
function EditorRSList() {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const controller = useRSEdit();
const isProcessing = useIsProcessingRSForm();
const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema?.items ?? []);
const [filterText, setFilterText] = useState('');
@ -91,7 +89,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
controller.deselectAll();
return;
}
if (!controller.isContentEditable || controller.isProcessing) {
if (!controller.isContentEditable || isProcessing) {
return;
}
if (event.key === 'Delete' && controller.canDeleteSelected) {
@ -170,7 +168,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
enableSelection={controller.isContentEditable}
selected={rowSelection}
setSelected={handleRowSelection}
onEdit={onOpenEdit}
onEdit={controller.navigateCst}
onCreateNew={() => controller.createCst(undefined, false)}
/>
</div>

View File

@ -1,3 +1,4 @@
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { CstTypeIcon } from '@/components/DomainIcons';
import {
IconClone,
@ -24,6 +25,7 @@ import { useRSEdit } from '../RSEditContext';
function ToolbarRSList() {
const controller = useRSEdit();
const isProcessing = useIsProcessingRSForm();
const insertMenu = useDropdown();
return (
@ -34,7 +36,7 @@ function ToolbarRSList() {
{controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS
items={controller.schema.oss}
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
/>
) : null}
<MiniButton
@ -47,7 +49,7 @@ function ToolbarRSList() {
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
disabled={
controller.isProcessing ||
isProcessing ||
controller.selected.length === 0 ||
(controller.schema && controller.selected.length === controller.schema.items.length)
}
@ -57,7 +59,7 @@ function ToolbarRSList() {
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
disabled={
controller.isProcessing ||
isProcessing ||
controller.selected.length === 0 ||
(controller.schema && controller.selected.length === controller.schema.items.length)
}
@ -68,7 +70,7 @@ function ToolbarRSList() {
title='Добавить пустую конституенту'
hideTitle={insertMenu.isOpen}
icon={<IconOpenList size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing}
disabled={isProcessing}
onClick={insertMenu.toggle}
/>
<Dropdown isOpen={insertMenu.isOpen} className='-translate-x-1/2 md:translate-x-0'>
@ -86,19 +88,19 @@ function ToolbarRSList() {
<MiniButton
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing}
disabled={isProcessing}
onClick={() => controller.createCst(undefined, false)}
/>
<MiniButton
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
icon={<IconClone size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing || controller.selected.length !== 1}
disabled={isProcessing || controller.selected.length !== 1}
onClick={controller.cloneCst}
/>
<MiniButton
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={controller.isProcessing || !controller.canDeleteSelected}
disabled={isProcessing || !controller.canDeleteSelected}
onClick={controller.promptDeleteCst}
/>
<BadgeHelp topic={HelpTopic.UI_RS_LIST} offset={5} />

View File

@ -1,17 +1,11 @@
import { ReactFlowProvider } from 'reactflow';
import { ConstituentaID } from '@/models/rsform';
import TGFlow from './TGFlow';
interface EditorTermGraphProps {
onOpenEdit: (cstID: ConstituentaID) => void;
}
function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
function EditorTermGraph() {
return (
<ReactFlowProvider>
<TGFlow onOpenEdit={onOpenEdit} />
<TGFlow />
</ReactFlowProvider>
);
}

View File

@ -19,6 +19,7 @@ import {
import { useStoreApi } from 'reactflow';
import { useDebounce } from 'use-debounce';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import InfoConstituenta from '@/components/info/InfoConstituenta';
import SelectedCounter from '@/components/info/SelectedCounter';
import { CProps } from '@/components/props';
@ -47,18 +48,13 @@ import ViewHidden from './ViewHidden';
const ZOOM_MAX = 3;
const ZOOM_MIN = 0.25;
interface TGFlowProps {
onOpenEdit: (cstID: ConstituentaID) => void;
}
function TGFlow({ onOpenEdit }: TGFlowProps) {
function TGFlow() {
const mainHeight = useMainHeight();
const controller = useRSEdit();
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges] = useEdgesState([]);
const flow = useReactFlow();
const store = useStoreApi();
const { addSelectedNodes } = store.getState();
const isProcessing = useIsProcessingRSForm();
const showParams = useDialogsStore(state => state.showGraphParams);
@ -67,6 +63,9 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
const coloring = useTermGraphStore(state => state.coloring);
const setColoring = useTermGraphStore(state => state.setColoring);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges] = useEdgesState([]);
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
const filteredGraph = useGraphFilter(controller.schema, filter, focusCst);
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
@ -113,7 +112,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
const resetNodes = useCallback(() => {
const newNodes: Node<TGNodeData>[] = [];
filteredGraph.nodes.forEach(node => {
const cst = controller.schema!.cstByID.get(node.id);
const cst = controller.schema.cstByID.get(node.id);
if (cst) {
newNodes.push({
id: String(node.id),
@ -184,7 +183,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
if (!controller.schema) {
return;
}
const definition = controller.selected.map(id => controller.schema!.cstByID.get(id)!.alias).join(' ');
const definition = controller.selected.map(id => controller.schema.cstByID.get(id)!.alias).join(' ');
controller.createCst(controller.selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
}
@ -232,7 +231,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
}
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (controller.isProcessing) {
if (isProcessing) {
return;
}
if (event.key === 'Escape') {
@ -282,7 +281,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
function handleNodeDoubleClick(event: CProps.EventMouse, cstID: ConstituentaID) {
event.preventDefault();
event.stopPropagation();
onOpenEdit(cstID);
controller.navigateCst(cstID);
}
function handleNodeEnter(event: CProps.EventMouse, cstID: ConstituentaID) {
@ -314,11 +313,11 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
/>
{!focusCst ? (
<ToolbarGraphSelection
graph={controller.schema!.graph}
graph={controller.schema.graph}
isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
isOwned={
controller.schema && controller.schema.inheritance.length > 0
? cstID => !controller.schema!.cstByID.get(cstID)?.is_inherited
? cstID => !controller.schema.cstByID.get(cstID)?.is_inherited
: undefined
}
selected={controller.selected}
@ -386,7 +385,6 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
coloringScheme={coloring}
toggleSelection={controller.toggleSelect}
setFocus={handleSetFocus}
onEdit={onOpenEdit}
/>
</div>
</Overlay>

View File

@ -1,5 +1,6 @@
import clsx from 'clsx';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import {
IconClustering,
IconClusteringOff,
@ -16,6 +17,7 @@ import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
import MiniButton from '@/components/ui/MiniButton';
import { HelpTopic } from '@/models/miscellaneous';
import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
@ -46,13 +48,24 @@ function ToolbarTermGraph({
onSaveImage
}: ToolbarTermGraphProps) {
const controller = useRSEdit();
const isProcessing = useIsProcessingRSForm();
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
function handleShowTypeGraph() {
const typeInfo = controller.schema?.items.map(item => ({
alias: item.alias,
result: item.parse.typification,
args: item.parse.args
}));
showTypeGraph({ items: typeInfo ?? [] });
}
return (
<div className='cc-icons'>
{controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS
items={controller.schema.oss}
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
/>
) : null}
<MiniButton
@ -91,7 +104,7 @@ function ToolbarTermGraph({
<MiniButton
title='Новая конституента'
icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing}
disabled={isProcessing}
onClick={onCreate}
/>
) : null}
@ -99,14 +112,14 @@ function ToolbarTermGraph({
<MiniButton
title='Удалить выбранные'
icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={!controller.canDeleteSelected || controller.isProcessing}
disabled={!controller.canDeleteSelected || isProcessing}
onClick={onDelete}
/>
) : null}
<MiniButton
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
title='Граф ступеней'
onClick={() => controller.showTypeGraph()}
onClick={handleShowTypeGraph}
/>
<MiniButton
icon={<IconImage size='1.25rem' className='icon-primary' />}

View File

@ -15,6 +15,8 @@ import { useTooltipsStore } from '@/stores/tooltips';
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
import { globals, PARAMETER, prefixes } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
interface ViewHiddenProps {
items: ConstituentaID[];
selected: ConstituentaID[];
@ -23,13 +25,13 @@ interface ViewHiddenProps {
toggleSelection: (cstID: ConstituentaID) => void;
setFocus: (cstID: ConstituentaID) => void;
onEdit: (cstID: ConstituentaID) => void;
}
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) {
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme }: ViewHiddenProps) {
const windowSize = useWindowSize();
const localSelected = items.filter(id => selected.includes(id));
const { navigateCst } = useRSEdit();
const isFolded = useTermGraphStore(state => state.foldHidden);
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
const setActiveCst = useTooltipsStore(state => state.setActiveCst);
@ -108,7 +110,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
: {})
}}
onClick={event => handleClick(cstID, event)}
onDoubleClick={() => onEdit(cstID)}
onDoubleClick={() => navigateCst(cstID)}
data-tooltip-id={globals.constituenta_tooltip}
onMouseEnter={() => setActiveCst(cst)}
>

View File

@ -1,9 +1,18 @@
'use client';
import { useGlobalOss } from '@/app/GlobalOssContext';
import fileDownload from 'js-file-download';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute';
import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm';
import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { useProduceStructure } from '@/backend/rsform/useProduceStructure';
import { useResetAliases } from '@/backend/rsform/useResetAliases';
import { useRestoreOrder } from '@/backend/rsform/useRestoreOrder';
import {
IconAdmin,
IconAlert,
@ -19,7 +28,6 @@ import {
IconLibrary,
IconMenu,
IconNewItem,
IconNewVersion,
IconOSS,
IconOwner,
IconQR,
@ -35,79 +43,142 @@ import Divider from '@/components/ui/Divider';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import useDropdown from '@/hooks/useDropdown';
import { AccessPolicy } from '@/models/library';
import { AccessPolicy, LocationHead } from '@/models/library';
import { CstType } from '@/models/rsform';
import { UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { useRoleStore } from '@/stores/role';
import { describeAccessMode, labelAccessMode, tooltips } from '@/utils/labels';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { describeAccessMode, information, labelAccessMode, tooltips } from '@/utils/labels';
import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils';
import { OssTabID } from '../OssPage/OssTabs';
import { OssTabID } from '../OssPage/OssEditContext';
import { useRSEdit } from './RSEditContext';
interface MenuRSTabsProps {
onDestroy: () => void;
}
function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
function MenuRSTabs() {
const controller = useRSEdit();
const router = useConceptNavigation();
const { user } = useAuth();
const oss = useGlobalOss();
const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole);
const { isModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const { resetAliases } = useResetAliases();
const { restoreOrder } = useRestoreOrder();
const { produceStructure } = useProduceStructure();
const { inlineSynthesis } = useInlineSynthesis();
const { cstSubstitute } = useCstSubstitute();
const { download } = useDownloadRSForm();
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
const showQR = useDialogsStore(state => state.showQR);
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
const showUpload = useDialogsStore(state => state.showUploadRSForm);
const schemaMenu = useDropdown();
const editMenu = useDropdown();
const accessMenu = useDropdown();
// TODO: move into separate function
const canProduceStructure =
!!controller.activeCst &&
!!controller.activeCst.parse.typification &&
controller.activeCst.cst_type !== CstType.BASE &&
controller.activeCst.cst_type !== CstType.CONSTANT;
function calculateCloneLocation() {
const location = controller.schema.location;
const head = location.substring(0, 2) as LocationHead;
if (head === LocationHead.LIBRARY) {
return user?.is_staff ? location : LocationHead.USER;
}
if (controller.schema.owner === user?.id) {
return location;
}
return head === LocationHead.USER ? LocationHead.USER : location;
}
function handleDelete() {
schemaMenu.hide();
onDestroy();
controller.deleteSchema();
}
function handleDownload() {
schemaMenu.hide();
controller.download();
if (isModified && !promptUnsaved()) {
return;
}
const fileName = (controller.schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
download({ itemID: controller.schema.id, version: controller.schema.version }, (data: Blob) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
}
function handleUpload() {
schemaMenu.hide();
controller.promptUpload();
showUpload({ itemID: controller.schema.id });
}
function handleClone() {
schemaMenu.hide();
controller.promptClone();
if (isModified && !promptUnsaved()) {
return;
}
showClone({
base: controller.schema,
initialLocation: calculateCloneLocation(),
selected: controller.selected,
totalCount: controller.schema.items.length
});
}
function handleShare() {
schemaMenu.hide();
controller.share();
sharePage();
}
function handleShowQR() {
schemaMenu.hide();
controller.showQR();
}
function handleCreateVersion() {
schemaMenu.hide();
controller.createVersion();
showQR({ target: generatePageQR() });
}
function handleReindex() {
editMenu.hide();
controller.reindex();
resetAliases(controller.schema.id, () => toast.success(information.reindexComplete));
}
function handleRestoreOrder() {
editMenu.hide();
controller.reorder();
restoreOrder(controller.schema.id, () => toast.success(information.reorderComplete));
}
function handleSubstituteCst() {
editMenu.hide();
controller.substitute();
if (isModified && !promptUnsaved()) {
return;
}
showSubstituteCst({
schema: controller.schema,
onSubstitute: data =>
cstSubstitute(
{
itemID: controller.schema.id, //
data
},
() => {
controller.setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)));
toast.success(information.substituteSingle);
}
)
});
}
function handleTemplates() {
@ -117,12 +188,41 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
function handleProduceStructure() {
editMenu.hide();
controller.produceStructure();
if (!controller.activeCst) {
return;
}
if (isModified && !promptUnsaved()) {
return;
}
produceStructure(
{
itemID: controller.schema.id, //
data: { target: controller.activeCst.id }
},
cstList => {
toast.success(information.addedConstituents(cstList.length));
if (cstList.length !== 0) {
controller.setSelected(cstList);
}
}
);
}
function handleInlineSynthesis() {
editMenu.hide();
controller.inlineSynthesis();
if (isModified && !promptUnsaved()) {
return;
}
showInlineSynthesis({
receiver: controller.schema,
onInlineSynthesis: data => {
const oldCount = controller.schema.items.length;
inlineSynthesis({ itemID: controller.schema.id, data }, newSchema => {
controller.deselectAll();
toast.success(information.addedConstituents(newSchema.items.length - oldCount));
});
}
});
}
function handleChangeMode(newMode: UserRole) {
@ -155,7 +255,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
<Dropdown isOpen={schemaMenu.isOpen}>
<DropdownButton
text='Поделиться'
titleHtml={tooltips.shareItem(controller.schema?.access_policy)}
titleHtml={tooltips.shareItem(controller.schema.access_policy)}
icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare}
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
@ -174,12 +274,6 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleClone}
/>
) : null}
<DropdownButton
text='Сохранить версию'
disabled={!controller.isContentEditable}
onClick={handleCreateVersion}
icon={<IconNewVersion size='1rem' className='icon-green' />}
/>
<DropdownButton
text='Выгрузить в Экстеор'
icon={<IconDownload size='1rem' className='icon-primary' />}
@ -189,7 +283,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
<DropdownButton
text='Загрузить из Экстеор'
icon={<IconUpload size='1rem' className='icon-red' />}
disabled={controller.isProcessing || controller.schema?.oss.length !== 0}
disabled={isProcessing || controller.schema?.oss.length !== 0}
onClick={handleUpload}
/>
) : null}
@ -197,7 +291,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
<DropdownButton
text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={controller.isProcessing || role < UserRole.OWNER}
disabled={isProcessing || role < UserRole.OWNER}
onClick={handleDelete}
/>
) : null}
@ -211,11 +305,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleCreateNew}
/>
) : null}
{oss.schema ? (
{controller.schema.oss.length > 0 ? (
<DropdownButton
text='Перейти к ОСС'
icon={<IconOSS size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.oss(oss.schema!.id, OssTabID.GRAPH))}
onClick={() => router.push(urls.oss(controller.schema.oss[0].id, OssTabID.GRAPH))}
/>
) : null}
<DropdownButton
@ -225,7 +319,6 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
/>
</Dropdown>
</div>
{!controller.isArchive && user ? (
<div ref={editMenu.ref}>
<Button
@ -244,14 +337,14 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
text='Шаблоны'
title='Создать конституенту из шаблона'
icon={<IconTemplates size='1rem' className='icon-green' />}
disabled={!controller.isContentEditable || controller.isProcessing}
disabled={!controller.isContentEditable || isProcessing}
onClick={handleTemplates}
/>
<DropdownButton
text='Встраивание'
titleHtml='Импортировать совокупность <br/>конституент из другой схемы'
icon={<IconInlineSynthesis size='1rem' className='icon-green' />}
disabled={!controller.isContentEditable || controller.isProcessing}
disabled={!controller.isContentEditable || isProcessing}
onClick={handleInlineSynthesis}
/>
@ -261,21 +354,21 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
text='Упорядочить список'
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
icon={<IconSortList size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || controller.isProcessing}
disabled={!controller.isContentEditable || isProcessing}
onClick={handleRestoreOrder}
/>
<DropdownButton
text='Порядковые имена'
titleHtml='Присвоить порядковые имена <br/>и обновить выражения'
icon={<IconGenerateNames size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || controller.isProcessing}
disabled={!controller.isContentEditable || isProcessing}
onClick={handleReindex}
/>
<DropdownButton
text='Порождение структуры'
titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты'
icon={<IconGenerateStructure size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || !controller.canProduceStructure || controller.isProcessing}
disabled={!controller.isContentEditable || !canProduceStructure || isProcessing}
onClick={handleProduceStructure}
/>
<DropdownButton
@ -283,11 +376,12 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
titleHtml='Заменить вхождения <br/>одной конституенты на другую'
icon={<IconReplace size='1rem' className='icon-red' />}
onClick={handleSubstituteCst}
disabled={!controller.isContentEditable || controller.isProcessing}
disabled={!controller.isContentEditable || isProcessing}
/>
</Dropdown>
</div>
) : null}
router.push(urls.schema(itemID, version), newTab);
{controller.isArchive && user ? (
<Button
dense
@ -298,10 +392,9 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
hideTitle={accessMenu.isOpen}
className='h-full px-2'
icon={<IconArchive size='1.25rem' className='icon-primary' />}
onClick={event => controller.viewVersion(undefined, event.ctrlKey || event.metaKey)}
onClick={event => router.push(urls.schema(controller.schema.id), event.ctrlKey || event.metaKey)}
/>
) : null}
{user ? (
<div ref={accessMenu.ref}>
<Button

View File

@ -1,69 +1,55 @@
'use client';
import fileDownload from 'js-file-download';
import { createContext, useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
import { useSetEditors } from '@/backend/library/useSetEditors';
import { useSetLocation } from '@/backend/library/useSetLocation';
import { useSetOwner } from '@/backend/library/useSetOwner';
import { useFindPredecessor } from '@/backend/oss/useFindPredecessor';
import { ICstCreateDTO, ICstRenameDTO, ICstUpdateDTO, IInlineSynthesisDTO } from '@/backend/rsform/api';
import { useDeleteItem } from '@/backend/library/useDeleteItem';
import { ICstCreateDTO } from '@/backend/rsform/api';
import { useCstCreate } from '@/backend/rsform/useCstCreate';
import { useCstDelete } from '@/backend/rsform/useCstDelete';
import { useCstMove } from '@/backend/rsform/useCstMove';
import { useCstRename } from '@/backend/rsform/useCstRename';
import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute';
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm';
import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { useProduceStructure } from '@/backend/rsform/useProduceStructure';
import { useResetAliases } from '@/backend/rsform/useResetAliases';
import { useRestoreOrder } from '@/backend/rsform/useRestoreOrder';
import { useRSFormSuspense } from '@/backend/rsform/useRSForm';
import {
AccessPolicy,
ILibraryItemEditor,
IVersionData,
LibraryItemID,
LocationHead,
VersionID
} from '@/models/library';
import { ICstSubstitutions } from '@/models/oss';
import { ConstituentaID, CstType, IConstituenta, IConstituentaMeta, IRSForm, TermForm } from '@/models/rsform';
import { ILibraryItemEditor, LibraryItemID, VersionID } from '@/models/library';
import { ConstituentaID, CstType, IConstituenta, IRSForm } from '@/models/rsform';
import { generateAlias } from '@/models/rsformAPI';
import { UserID, UserRole } from '@/models/user';
import { UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
import { useRoleStore } from '@/stores/role';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { PARAMETER, prefixes } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
import { RSTabID } from './RSTabs';
import { OssTabID } from '../OssPage/OssEditContext';
export enum RSTabID {
CARD = 0,
CST_LIST = 1,
CST_EDIT = 2,
TERM_GRAPH = 3
}
export interface IRSEditContext extends ILibraryItemEditor {
schema?: IRSForm;
schema: IRSForm;
selected: ConstituentaID[];
activeCst?: IConstituenta;
isOwned: boolean;
isArchive: boolean;
isMutable: boolean;
isContentEditable: boolean;
isProcessing: boolean;
isAttachedToOSS: boolean;
canProduceStructure: boolean;
canDeleteSelected: boolean;
setOwner: (newOwner: UserID) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void;
promptLocation: () => void;
navigateRSForm: ({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) => void;
navigateCst: (cstID: ConstituentaID) => void;
navigateOss: (target: LibraryItemID, newTab?: boolean) => void;
deleteSchema: () => void;
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
select: (target: ConstituentaID) => void;
@ -71,35 +57,12 @@ export interface IRSEditContext extends ILibraryItemEditor {
toggleSelect: (target: ConstituentaID) => void;
deselectAll: () => void;
viewOSS: (target: LibraryItemID, newTab?: boolean) => void;
viewVersion: (version?: VersionID, newTab?: boolean) => void;
viewPredecessor: (target: ConstituentaID) => void;
createVersion: () => void;
restoreVersion: () => void;
promptEditVersions: () => void;
moveUp: () => void;
moveDown: () => void;
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
renameCst: () => void;
cloneCst: () => void;
promptDeleteCst: () => void;
editTermForms: () => void;
promptTemplate: () => void;
promptClone: () => void;
promptUpload: () => void;
share: () => void;
download: () => void;
reindex: () => void;
reorder: () => void;
produceStructure: () => void;
inlineSynthesis: () => void;
substitute: () => void;
showTypeGraph: () => void;
showQR: () => void;
}
const RSEditContext = createContext<IRSEditContext | null>(null);
@ -113,28 +76,11 @@ export const useRSEdit = () => {
interface RSEditStateProps {
itemID: LibraryItemID;
activeTab: RSTabID;
versionID?: VersionID;
selected: ConstituentaID[];
isModified: boolean;
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
activeCst?: IConstituenta;
onCreateCst?: (newCst: IConstituentaMeta) => void;
onDeleteCst?: (newActive?: ConstituentaID) => void;
}
export const RSEditState = ({
itemID,
versionID,
selected,
setSelected,
activeCst,
isModified,
onCreateCst,
onDeleteCst,
children
}: React.PropsWithChildren<RSEditStateProps>) => {
export const RSEditState = ({ itemID, versionID, activeTab, children }: React.PropsWithChildren<RSEditStateProps>) => {
const router = useConceptNavigation();
const { user } = useAuth();
const adminMode = usePreferencesStore(state => state.adminMode);
@ -142,72 +88,33 @@ export const RSEditState = ({
const adjustRole = useRoleStore(state => state.adjustRole);
const { schema } = useRSFormSuspense({ itemID: itemID, version: versionID });
const isProcessing = useIsProcessingRSForm();
const { download: downloadFile } = useDownloadRSForm();
const { findPredecessor } = useFindPredecessor();
const { setOwner: setItemOwner } = useSetOwner();
const { setLocation: setItemLocation } = useSetLocation();
const { setAccessPolicy: setItemAccessPolicy } = useSetAccessPolicy();
const { setEditors: setItemEditors } = useSetEditors();
const { cstCreate } = useCstCreate();
const { cstRename } = useCstRename();
const { cstSubstitute } = useCstSubstitute();
const { cstMove } = useCstMove();
const { cstDelete } = useCstDelete();
const { cstUpdate } = useCstUpdate();
const { produceStructure: produceStructureInternal } = useProduceStructure();
const { inlineSynthesis: inlineSynthesisInternal } = useInlineSynthesis();
const { restoreOrder: restoreOrderInternal } = useRestoreOrder();
const { resetAliases: resetAliasesInternal } = useResetAliases();
const { isModified } = useModificationStore();
const isOwned = user?.id === schema?.owner || false;
const isArchive = !!versionID;
const isMutable = role > UserRole.READER && !schema?.read_only;
const isMutable = role > UserRole.READER && !schema.read_only;
const isContentEditable = isMutable && !isArchive;
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema?.cstByID.get(id)?.is_inherited);
const isAttachedToOSS =
!!schema && schema.oss.length > 0 && (schema.stats.count_inherited > 0 || schema.items.length === 0);
const isAttachedToOSS = schema.oss.length > 0;
const [renameInitialData, setRenameInitialData] = useState<ICstRenameDTO>();
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
const showCreateVersion = useDialogsStore(state => state.showCreateVersion);
const showEditVersions = useDialogsStore(state => state.showEditVersions);
const showEditEditors = useDialogsStore(state => state.showEditEditors);
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
const activeCst: IConstituenta | undefined = (() => {
if (!schema || selected.length === 0) {
return undefined;
} else {
return schema.cstByID.get(selected[-1]);
}
})();
const { cstCreate } = useCstCreate();
const { cstMove } = useCstMove();
const { cstDelete } = useCstDelete();
const { deleteItem } = useDeleteItem();
const showCreateCst = useDialogsStore(state => state.showCreateCst);
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
const showRenameCst = useDialogsStore(state => state.showRenameCst);
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
const showUpload = useDialogsStore(state => state.showUploadRSForm);
const showQR = useDialogsStore(state => state.showQR);
const typeInfo = schema
? schema.items.map(item => ({
alias: item.alias,
result: item.parse.typification,
args: item.parse.args
}))
: [];
const canProduceStructure =
!!activeCst &&
!!activeCst.parse.typification &&
activeCst.cst_type !== CstType.BASE &&
activeCst.cst_type !== CstType.CONSTANT;
useEffect(
() =>
@ -220,79 +127,78 @@ export const RSEditState = ({
[schema, adjustRole, isOwned, user, adminMode]
);
function viewVersion(version?: VersionID, newTab?: boolean) {
router.push(urls.schema(itemID, version), newTab);
}
function viewPredecessor(target: ConstituentaID) {
findPredecessor({ target: target }, reference =>
router.push(
urls.schema_props({
id: reference.schema,
active: reference.id,
tab: RSTabID.CST_EDIT
})
)
);
}
function viewOSS(target: LibraryItemID, newTab?: boolean) {
function navigateOss(target: LibraryItemID, newTab?: boolean) {
router.push(urls.oss(target), newTab);
}
function restoreVersion() {
if (!versionID || !window.confirm(prompts.restoreArchive)) {
function navigateRSForm({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) {
if (!schema) {
return;
}
model.versionRestore(versionID, () => {
toast.success(information.versionRestored);
viewVersion(undefined);
const data = {
id: schema.id,
tab: tab,
active: activeID,
version: versionID
};
const url = urls.schema_props(data);
if (activeID) {
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
router.replace(url);
} else {
router.push(url);
}
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
data.active = schema.items[0].id;
router.replace(urls.schema_props(data));
} else {
router.push(url);
}
}
function navigateCst(cstID: ConstituentaID) {
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
navigateRSForm({ tab: RSTabID.CST_EDIT, activeID: cstID });
}
}
function deleteSchema() {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
return;
}
const ossID = schema.oss.length > 0 ? schema.oss[0].id : undefined;
deleteItem(schema.id, () => {
toast.success(information.itemDestroyed);
if (ossID) {
router.push(urls.oss(ossID, OssTabID.GRAPH));
} else {
router.push(urls.library);
}
});
}
function calculateCloneLocation() {
if (!schema) {
return LocationHead.USER;
}
const location = schema.location;
const head = schema.location.substring(0, 2) as LocationHead;
if (head === LocationHead.LIBRARY) {
return user?.is_staff ? location : LocationHead.USER;
}
if (schema.owner === user?.id) {
return location;
}
return head === LocationHead.USER ? LocationHead.USER : location;
}
function handleCreateCst(data: ICstCreateDTO) {
if (!schema) {
return;
}
data.alias = data.alias || generateAlias(data.cst_type, schema);
cstCreate({ itemID: itemID, data }, newCst => {
toast.success(information.newConstituent(newCst.alias));
setSelected([newCst.id]);
if (onCreateCst) onCreateCst(newCst);
});
}
function handleRenameCst(data: ICstRenameDTO) {
const oldAlias = renameInitialData?.alias ?? '';
cstRename({ itemID: itemID, data }, () => toast.success(information.renameComplete(oldAlias, data.alias)));
}
function handleSubstituteCst(data: ICstSubstitutions) {
cstSubstitute({ itemID: itemID, data }, () => {
setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)));
toast.success(information.substituteSingle);
navigateRSForm({ tab: activeTab, activeID: newCst.id });
if (activeTab === RSTabID.CST_LIST) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'end'
});
}
}, PARAMETER.refreshTimeout);
}
});
}
function handleDeleteCst(deleted: ConstituentaID[]) {
if (!schema) {
return;
}
const data = {
items: deleted
};
@ -304,88 +210,18 @@ export const RSEditState = ({
cstDelete({ itemID: itemID, data }, () => {
toast.success(information.constituentsDestroyed(deletedNames));
setSelected(nextActive ? [nextActive] : []);
onDeleteCst?.(nextActive);
});
}
function handleSaveWordforms(forms: TermForm[]) {
if (!activeCst) {
return;
}
const data: ICstUpdateDTO = {
target: activeCst.id,
item_data: { term_forms: forms }
};
cstUpdate({ itemID: itemID, data }, () => toast.success(information.changesSaved));
}
function handleCreateVersion(data: IVersionData) {
if (!schema) {
return;
}
model.versionCreate(data, () => {
toast.success(information.newVersion(data.version));
});
}
function handleDeleteVersion(versionID: VersionID) {
if (!schema) {
return;
}
model.versionDelete(versionID, () => {
toast.success(information.versionDestroyed);
if (versionID === versionID) {
viewVersion(undefined);
if (!nextActive) {
navigateRSForm({ tab: RSTabID.CST_LIST });
} else if (activeTab === RSTabID.CST_EDIT) {
navigateRSForm({ tab: activeTab, activeID: nextActive });
} else {
navigateRSForm({ tab: activeTab });
}
});
}
function handleUpdateVersion(versionID: VersionID, data: IVersionData) {
if (!schema) {
return;
}
model.versionUpdate(versionID, data, () => toast.success(information.changesSaved));
}
const handleSetLocation = (newLocation: string) =>
setItemLocation({ itemID: itemID, location: newLocation }, () => toast.success(information.moveComplete));
function handleInlineSynthesis(data: IInlineSynthesisDTO) {
if (!schema) {
return;
}
const oldCount = schema.items.length;
inlineSynthesisInternal({ itemID: itemID, data }, newSchema => {
setSelected([]);
toast.success(information.addedConstituents(newSchema.items.length - oldCount));
});
}
function createVersion() {
if (!schema || (isModified && !promptUnsaved())) {
return;
}
showCreateVersion({
versions: schema.versions,
onCreate: handleCreateVersion,
selected: selected,
totalCount: schema.items.length
});
}
function promptEditVersions() {
if (!schema) {
return;
}
showEditVersions({
versions: schema.versions,
onDelete: handleDeleteVersion,
onUpdate: handleUpdateVersion
});
}
function moveUp() {
if (!schema?.items || selected.length === 0) {
if (!schema.items || selected.length === 0) {
return;
}
const currentIndex = schema.items.reduce((prev, cst, index) => {
@ -407,7 +243,7 @@ export const RSEditState = ({
}
function moveDown() {
if (!schema?.items || selected.length === 0) {
if (!schema.items || selected.length === 0) {
return;
}
let count = 0;
@ -433,9 +269,6 @@ export const RSEditState = ({
}
function createCst(type: CstType | undefined, skipDialog: boolean, definition?: string) {
if (!schema) {
return;
}
const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
const data: ICstCreateDTO = {
insert_after: activeCst?.id ?? null,
@ -455,7 +288,7 @@ export const RSEditState = ({
}
function cloneCst() {
if (!activeCst || !schema) {
if (!activeCst) {
return;
}
const data: ICstCreateDTO = {
@ -471,213 +304,50 @@ export const RSEditState = ({
handleCreateCst(data);
}
function renameCst() {
if (!activeCst || !schema) {
return;
}
const data: ICstRenameDTO = {
target: activeCst.id,
alias: activeCst.alias,
cst_type: activeCst.cst_type
};
setRenameInitialData(data);
showRenameCst({
schema: schema,
initial: data,
allowChangeType: !activeCst.is_inherited,
onRename: handleRenameCst
});
}
function substitute() {
if (!schema || (isModified && !promptUnsaved())) {
return;
}
showSubstituteCst({ schema: schema, onSubstitute: handleSubstituteCst });
}
function inlineSynthesis() {
if (!schema || (isModified && !promptUnsaved())) {
return;
}
showInlineSynthesis({ receiver: schema, onInlineSynthesis: handleInlineSynthesis });
}
function promptDeleteCst() {
if (!schema) {
return;
}
showDeleteCst({ schema: schema, selected: selected, onDelete: handleDeleteCst });
}
function editTermForms() {
if (!activeCst) {
return;
}
if (isModified && !promptUnsaved()) {
return;
}
showEditTerm({ target: activeCst, onSave: handleSaveWordforms });
}
function reindex() {
if (!itemID) {
return;
}
resetAliasesInternal(itemID, () => toast.success(information.reindexComplete));
}
function reorder() {
if (!itemID) {
return;
}
restoreOrderInternal(itemID, () => toast.success(information.reorderComplete));
}
function produceStructure() {
if (!activeCst) {
return;
}
if (isModified && !promptUnsaved()) {
return;
}
produceStructureInternal({ itemID: itemID, data: { target: activeCst.id } }, cstList => {
toast.success(information.addedConstituents(cstList.length));
if (cstList.length !== 0) {
setSelected(cstList);
}
});
}
function handleSetEditors(newEditors: UserID[]) {
setItemEditors({ itemID: itemID, editors: newEditors }, () => toast.success(information.changesSaved));
}
function promptTemplate() {
if ((isModified && !promptUnsaved()) || !schema) {
if (isModified && !promptUnsaved()) {
return;
}
showCstTemplate({ schema: schema, onCreate: handleCreateCst, insertAfter: activeCst?.id });
}
function promptClone() {
if (!schema || (isModified && !promptUnsaved())) {
return;
}
showClone({
base: schema,
initialLocation: calculateCloneLocation(),
selected: selected,
totalCount: schema.items.length
});
}
function promptEditors() {
if (!schema) {
return;
}
showEditEditors({ editors: schema.editors, setEditors: handleSetEditors });
}
function promptLocation() {
if (!schema) {
return;
}
showEditLocation({ initial: schema.location, onChangeLocation: handleSetLocation });
}
function download() {
if ((isModified && !promptUnsaved()) || !schema) {
return;
}
const fileName = (schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
downloadFile({ itemID: itemID, version: versionID }, (data: Blob) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
}
function share() {
const currentRef = window.location.href;
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
navigator.clipboard
.writeText(url)
.then(() => toast.success(information.linkReady))
.catch(console.error);
}
function setOwner(newOwner: UserID) {
setItemOwner({ itemID: itemID, owner: newOwner }, () => toast.success(information.changesSaved));
}
function setAccessPolicy(newPolicy: AccessPolicy) {
setItemAccessPolicy({ itemID: itemID, policy: newPolicy }, () => toast.success(information.changesSaved));
}
function generateQR(): string {
const currentRef = window.location.href;
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
}
return (
<RSEditContext
value={{
schema: schema,
schema,
selected,
activeCst,
isOwned,
isArchive,
isMutable,
isContentEditable,
isProcessing,
isAttachedToOSS,
canProduceStructure,
canDeleteSelected,
setOwner,
setAccessPolicy,
promptEditors,
promptLocation,
navigateRSForm,
navigateCst,
setSelected: setSelected,
deleteSchema,
navigateOss,
setSelected,
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
toggleSelect: (target: ConstituentaID) =>
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
deselectAll: () => setSelected([]),
viewOSS,
viewVersion,
viewPredecessor,
createVersion,
restoreVersion,
promptEditVersions,
moveUp,
moveDown,
createCst,
cloneCst,
renameCst,
promptDeleteCst,
editTermForms,
promptTemplate,
promptClone,
promptUpload: () => showUpload({ itemID: model.itemID! }),
download,
share,
reindex,
reorder,
inlineSynthesis,
produceStructure,
substitute,
showTypeGraph: () => showTypeGraph({ items: typeInfo }),
showQR: () => showQR({ target: generateQR() })
promptTemplate
}}
>
{children}

View File

@ -1,21 +1,81 @@
'use client';
import axios from 'axios';
import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router';
import { RSFormState } from '@/context/RSFormContext';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import Divider from '@/components/ui/Divider';
import TextURL from '@/components/ui/TextURL';
import useQueryStrings from '@/hooks/useQueryStrings';
import { LibraryItemID } from '@/models/library';
import { useModificationStore } from '@/stores/modification';
import { RSEditState, RSTabID } from './RSEditContext';
import RSTabs from './RSTabs';
function RSFormPage() {
const params = useParams();
const router = useConceptNavigation();
const query = useQueryStrings();
const version = query.get('v') ?? undefined;
const params = useParams();
const itemID = params.id ? Number(params.id) : undefined;
const version = query.get('v') ? Number(query.get('v')) : undefined;
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
const { isModified } = useModificationStore();
useBlockNavigation(isModified);
if (!itemID) {
router.replace(urls.page404);
return null;
}
return (
<RSFormState itemID={params.id ?? ''} versionID={version}>
<RSTabs />
</RSFormState>
<ErrorBoundary
FallbackComponent={({ error }) => (
<ProcessError error={error as ErrorData} isArchive={!!version} itemID={itemID} />
)}
>
<RSEditState itemID={itemID} versionID={version} activeTab={activeTab}>
<RSTabs />
</RSEditState>
</ErrorBoundary>
);
}
export default RSFormPage;
// ====== Internals =========
function ProcessError({
error,
isArchive,
itemID
}: {
error: ErrorData;
isArchive: boolean;
itemID?: LibraryItemID;
}): React.ReactElement {
if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 404) {
return (
<div className='flex flex-col items-center p-2 mx-auto'>
<p>{`Концептуальная схема с указанным идентификатором ${isArchive ? 'и версией ' : ''}отсутствует`}</p>
<div className='flex justify-center'>
<TextURL text='Библиотека' href='/library' />
{isArchive ? <Divider vertical margins='mx-3' /> : null}
{isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${itemID}`} /> : null}
</div>
</div>
);
} else if (error.response.status === 403) {
return (
<div className='flex flex-col items-center p-2 mx-auto'>
<p>Владелец ограничил доступ к данной схеме</p>
<TextURL text='Библиотека' href='/library' />
</div>
);
}
}
return <InfoError error={error} />;
}

View File

@ -1,79 +1,43 @@
'use client';
import axios from 'axios';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
import { useEffect } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify';
import { useGlobalOss } from '@/app/GlobalOssContext';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useDeleteItem } from '@/backend/library/useDeleteItem';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import Divider from '@/components/ui/Divider';
import Loader from '@/components/ui/Loader';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL';
import useQueryStrings from '@/hooks/useQueryStrings';
import { LibraryItemID } from '@/models/library';
import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsform';
import { useAppLayoutStore } from '@/stores/appLayout';
import { PARAMETER, prefixes } from '@/utils/constants';
import { information, labelVersion, prompts } from '@/utils/labels';
import { useModificationStore } from '@/stores/modification';
import { labelVersion } from '@/utils/labels';
import { OssTabID } from '../OssPage/OssTabs';
import EditorConstituenta from './EditorConstituenta';
import EditorRSForm from './EditorRSFormCard';
import EditorRSList from './EditorRSList';
import EditorTermGraph from './EditorTermGraph';
import MenuRSTabs from './MenuRSTabs';
import { RSEditState } from './RSEditContext';
export enum RSTabID {
CARD = 0,
CST_LIST = 1,
CST_EDIT = 2,
TERM_GRAPH = 3
}
import { RSTabID, useRSEdit } from './RSEditContext';
function RSTabs() {
const params = useParams();
const router = useConceptNavigation();
const query = useQueryStrings();
const router = useConceptNavigation();
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
const itemID = params.id ? Number(params.id) : undefined;
const version = query.get('v') ? Number(query.get('v')) : undefined;
const cstQuery = query.get('active');
const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { schema, loading, errorLoading, isArchive } = useRSFormControl();
const { deleteItem } = useDeleteItem();
const oss = useGlobalOss();
const { setIsModified } = useModificationStore();
const { schema, selected, setSelected, navigateRSForm } = useRSEdit();
const [isModified, setIsModified] = useState(false);
useBlockNavigation(isModified);
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const activeCst: IConstituenta | undefined = (() => {
if (!schema || selected.length === 0) {
return undefined;
} else {
return schema.cstByID.get(selected.at(-1)!);
}
})();
useEffect(() => setIsModified(false), [setIsModified]);
useEffect(() => {
if (schema) {
const oldTitle = document.title;
document.title = schema.title;
return () => {
document.title = oldTitle;
};
}
}, [schema, schema?.title]);
const oldTitle = document.title;
document.title = schema.title;
return () => {
document.title = oldTitle;
};
}, [schema?.title]);
useEffect(() => {
hideFooter(activeTab !== RSTabID.CARD);
@ -89,30 +53,6 @@ function RSTabs() {
return () => hideFooter(false);
}, [activeTab, cstQuery, setSelected, schema, hideFooter, setIsModified]);
function navigateTab(tab: RSTabID, activeID?: ConstituentaID) {
if (!schema) {
return;
}
const url = urls.schema_props({
id: schema.id,
tab: tab,
active: activeID,
version: version
});
if (activeID) {
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
router.replace(url);
} else {
router.push(url);
}
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
activeID = schema.items[0].id;
router.replace(url);
} else {
router.push(url);
}
}
function onSelectTab(index: number, last: number, event: Event) {
if (last === index) {
return;
@ -129,160 +69,54 @@ function RSTabs() {
}
}
}
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined);
}
function onCreateCst(newCst: IConstituentaMeta) {
navigateTab(activeTab, newCst.id);
if (activeTab === RSTabID.CST_LIST) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'end'
});
}
}, PARAMETER.refreshTimeout);
}
}
function onDeleteCst(newActive?: ConstituentaID) {
if (!newActive) {
navigateTab(RSTabID.CST_LIST);
} else if (activeTab === RSTabID.CST_EDIT) {
navigateTab(activeTab, newActive);
} else {
navigateTab(activeTab);
}
}
function onOpenCst(cstID: ConstituentaID) {
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
navigateTab(RSTabID.CST_EDIT, cstID);
}
}
function onDestroySchema() {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
return;
}
const backToOSS = oss.schema?.schemas.includes(schema.id);
deleteItem(schema.id, () => {
toast.success(information.itemDestroyed);
if (backToOSS) {
oss
.invalidate()
.then(() => router.push(urls.oss(oss.schema!.id, OssTabID.GRAPH)))
.catch(console.error);
} else {
router.push(urls.library);
}
});
navigateRSForm({ tab: index, activeID: selected.length > 0 ? selected.at(-1) : undefined });
}
return (
<RSEditState
selected={selected}
setSelected={setSelected}
activeCst={activeCst}
isModified={isModified}
onCreateCst={onCreateCst}
onDeleteCst={onDeleteCst}
>
{loading ? <Loader /> : null}
{errorLoading ? <ProcessError error={errorLoading} isArchive={isArchive} itemID={itemID} /> : null}
{schema && !loading ? (
<Tabs
selectedIndex={activeTab}
onSelect={onSelectTab}
defaultFocus
selectedTabClassName='clr-selected'
className='flex flex-col mx-auto min-w-fit'
>
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
<TabList
className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}
>
<MenuRSTabs onDestroy={onDestroySchema} />
<>
<Tabs
selectedIndex={activeTab}
onSelect={onSelectTab}
defaultFocus
selectedTabClassName='clr-selected'
className='flex flex-col mx-auto min-w-fit'
>
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
<TabList
className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}
>
<MenuRSTabs />
<TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} />
<TabLabel
label='Содержание'
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${
schema.stats?.count_errors ?? 0
}`}
/>
<TabLabel label='Редактор' />
<TabLabel label='Граф термов' />
</TabList>
</Overlay>
<TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} />
<TabLabel
label='Содержание'
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${schema.stats?.count_errors ?? 0}`}
/>
<TabLabel label='Редактор' />
<TabLabel label='Граф термов' />
</TabList>
</Overlay>
<div className='overflow-x-hidden'>
<TabPanel>
<EditorRSForm
isModified={isModified} //
setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
<div className='overflow-x-hidden'>
<TabPanel>
<EditorRSForm />
</TabPanel>
<TabPanel>
<EditorRSList onOpenEdit={onOpenCst} />
</TabPanel>
<TabPanel>
<EditorRSList />
</TabPanel>
<TabPanel>
<EditorConstituenta
isModified={isModified}
setIsModified={setIsModified}
activeCst={activeCst}
onOpenEdit={onOpenCst}
/>
</TabPanel>
<TabPanel>
<EditorConstituenta />
</TabPanel>
<TabPanel>
<EditorTermGraph onOpenEdit={onOpenCst} />
</TabPanel>
</div>
</Tabs>
) : null}
</RSEditState>
<TabPanel>
<EditorTermGraph />
</TabPanel>
</div>
</Tabs>
</>
);
}
export default RSTabs;
// ====== Internals =========
function ProcessError({
error,
isArchive,
itemID
}: {
error: ErrorData;
isArchive: boolean;
itemID?: LibraryItemID;
}): React.ReactElement {
if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 404) {
return (
<div className='flex flex-col items-center p-2 mx-auto'>
<p>{`Концептуальная схема с указанным идентификатором ${isArchive ? 'и версией ' : ''}отсутствует`}</p>
<div className='flex justify-center'>
<TextURL text='Библиотека' href='/library' />
{isArchive ? <Divider vertical margins='mx-3' /> : null}
{isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${itemID}`} /> : null}
</div>
</div>
);
} else if (error.response.status === 403) {
return (
<div className='flex flex-col items-center p-2 mx-auto'>
<p>Владелец ограничил доступ к данной схеме</p>
<TextURL text='Библиотека' href='/library' />
</div>
);
}
}
return <InfoError error={error} />;
}

View File

@ -4,12 +4,13 @@ import clsx from 'clsx';
import { useState } from 'react';
import useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { IConstituenta } from '@/models/rsform';
import { UserRole } from '@/models/user';
import { useFitHeight } from '@/stores/appLayout';
import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
import ConstituentsSearch from './ConstituentsSearch';
import TableSideConstituents from './TableSideConstituents';
@ -19,16 +20,14 @@ const COLUMN_DENSE_SEARCH_THRESHOLD = 1100;
interface ViewConstituentsProps {
expression: string;
isBottom?: boolean;
activeCst?: IConstituenta;
schema?: IRSForm;
onOpenEdit: (cstID: ConstituentaID) => void;
isMounted: boolean;
}
function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit, isMounted }: ViewConstituentsProps) {
function ViewConstituents({ expression, isBottom, isMounted }: ViewConstituentsProps) {
const windowSize = useWindowSize();
const role = useRoleStore(state => state.role);
const listHeight = useFitHeight(!isBottom ? '8.2rem' : role !== UserRole.READER ? '42rem' : '35rem', '10rem');
const { schema, activeCst, navigateCst } = useRSEdit();
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
@ -60,7 +59,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
maxHeight={listHeight}
items={filteredData}
activeCst={activeCst}
onOpenEdit={onOpenEdit}
onOpenEdit={navigateCst}
autoScroll={!isBottom}
/>
</div>

View File

@ -0,0 +1,11 @@
import { create } from 'zustand';
interface ModificationStore {
isModified: boolean;
setIsModified: (value: boolean) => void;
}
export const useModificationStore = create<ModificationStore>()(set => ({
isModified: false,
setIsModified: value => set({ isModified: value })
}));

View File

@ -3,10 +3,11 @@
*/
import axios, { AxiosError, AxiosHeaderValue, AxiosResponse } from 'axios';
import { toast } from 'react-toastify';
import { AliasMapping } from '@/models/rslang';
import { prompts } from './labels';
import { information, prompts } from './labels';
/**
* Checks if arguments is Node.
@ -202,3 +203,23 @@ export function convertToCSV(targetObj: object[]): Blob {
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
}
/**
* Generates a QR code for the current page.
*/
export function generatePageQR(): string {
const currentRef = window.location.href;
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
}
/**
* Copies sharable link to the current page.
*/
export function sharePage() {
const currentRef = window.location.href;
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
navigator.clipboard
.writeText(url)
.then(() => toast.success(information.linkReady))
.catch(console.error);
}