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

View File

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

View File

@ -1,8 +1,12 @@
import { useIsMutating } from '@tanstack/react-query'; import { useIsMutating } from '@tanstack/react-query';
import { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
import { libraryApi } from './api'; import { libraryApi } from './api';
export const useIsProcessingLibrary = () => { export const useIsProcessingLibrary = () => {
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] }); 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 = { export const ossApi = {
baseKey: 'library', baseKey: 'oss',
getOssQueryOptions: ({ itemID }: { itemID?: LibraryItemID }) => { getOssQueryOptions: ({ itemID }: { itemID?: LibraryItemID }) => {
return queryOptions({ return queryOptions({

View File

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

View File

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

View File

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

View File

@ -15,11 +15,11 @@ export function useRSForm({ itemID, version }: { itemID?: LibraryItemID; version
return { schema, isLoading, error }; 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({ const { data } = useSuspenseQuery({
...rsformsApi.getRSFormQueryOptions({ itemID, version }) ...rsformsApi.getRSFormQueryOptions({ itemID, version })
}); });
const schema = data ? new RSFormLoader(data).produceRSForm() : undefined; const schema = new RSFormLoader(data!).produceRSForm();
return { schema }; return { schema };
} }

View File

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

View File

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

View File

@ -100,16 +100,9 @@ export interface ILibraryItemVersioned extends ILibraryItemData {
* Represents common {@link ILibraryItem} editor controller. * Represents common {@link ILibraryItem} editor controller.
*/ */
export interface ILibraryItemEditor { export interface ILibraryItemEditor {
schema?: ILibraryItemData; schema: ILibraryItemData;
deleteSchema: () => void;
isMutable: boolean; isMutable: boolean;
isProcessing: boolean;
isAttachedToOSS: 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 { useEffect } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth'; import { useAuth } from '@/backend/auth/useAuth';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
function HomePage() { function HomePage() {

View File

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

View File

@ -3,22 +3,19 @@
import clsx from 'clsx'; import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import { LibraryItemType } from '@/models/library';
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem'; import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard'; import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard';
import { useModificationStore } from '@/stores/modification';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
import FormOSS from './FormOSS'; import FormOSS from './FormOSS';
import OssStats from './OssStats'; import OssStats from './OssStats';
interface EditorOssCardProps { function EditorOssCard() {
isModified: boolean;
setIsModified: (newValue: boolean) => void;
onDestroy: () => void;
}
function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const { isModified } = useModificationStore();
function initiateSubmit() { function initiateSubmit() {
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement; const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
@ -38,12 +35,7 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
return ( return (
<> <>
<ToolbarRSFormCard <ToolbarRSFormCard onSubmit={initiateSubmit} controller={controller} />
modified={isModified}
onSubmit={initiateSubmit}
onDestroy={onDestroy}
controller={controller}
/>
<div <div
onKeyDown={handleInput} onKeyDown={handleInput}
className={clsx( className={clsx(
@ -54,8 +46,8 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
)} )}
> >
<FlexColumn className='px-3'> <FlexColumn className='px-3'>
<FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} /> <FormOSS id={globals.library_item_editor} />
<EditorLibraryItem item={controller.schema} isModified={isModified} controller={controller} /> <EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.OSS} controller={controller} />
</FlexColumn> </FlexColumn>
{controller.schema ? <OssStats stats={controller.schema.stats} /> : null} {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 { ILibraryUpdateDTO } from '@/backend/library/api';
import { useUpdateItem } from '@/backend/library/useUpdateItem'; import { useUpdateItem } from '@/backend/library/useUpdateItem';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
import { IconSave } from '@/components/Icons'; import { IconSave } from '@/components/Icons';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { LibraryItemType } from '@/models/library'; import { LibraryItemType } from '@/models/library';
import ToolbarItemAccess from '@/pages/RSFormPage/EditorRSFormCard/ToolbarItemAccess'; import ToolbarItemAccess from '@/pages/RSFormPage/EditorRSFormCard/ToolbarItemAccess';
import { useModificationStore } from '@/stores/modification';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
interface FormOSSProps { interface FormOSSProps {
id?: string; id?: string;
isModified: boolean;
setIsModified: (newValue: boolean) => void;
} }
function FormOSS({ id, isModified, setIsModified }: FormOSSProps) { function FormOSS({ id }: FormOSSProps) {
const { updateItem: update } = useUpdateItem(); const { updateItem: update } = useUpdateItem();
const controller = useOssEdit(); const controller = useOssEdit();
const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingOss();
const schema = controller.schema; const schema = controller.schema;
const [title, setTitle] = useState(schema?.title ?? ''); const [title, setTitle] = useState(schema?.title ?? '');
@ -125,14 +127,14 @@ function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
label='Описание' label='Описание'
rows={3} rows={3}
value={comment} value={comment}
disabled={!controller.isMutable || controller.isProcessing} disabled={!controller.isMutable || isProcessing}
onChange={event => setComment(event.target.value)} onChange={event => setComment(event.target.value)}
/> />
{controller.isMutable || isModified ? ( {controller.isMutable || isModified ? (
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
className='self-center mt-4' className='self-center mt-4'
loading={controller.isProcessing} loading={isProcessing}
disabled={!isModified} disabled={!isModified}
icon={<IconSave size='1.25rem' />} icon={<IconSave size='1.25rem' />}
/> />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +1,69 @@
'use client'; 'use client';
import { Suspense } from 'react'; import axios from 'axios';
import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import Loader from '@/components/ui/Loader'; import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { OssState } from '@/pages/OssPage/OssContext'; 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'; import OssTabs from './OssTabs';
function OssPage() { function OssPage() {
const router = useConceptNavigation();
const params = useParams(); const params = useParams();
const itemID = params.id ? Number(params.id) : undefined; 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 ( return (
<Suspense fallback={<Loader />}> <ErrorBoundary FallbackComponent={ProcessError}>
<OssState itemID={itemID}> <OssEditState itemID={itemID}>
<OssTabs /> <OssTabs />
</OssState> </OssEditState>
</Suspense> </ErrorBoundary>
); );
} }
export default OssPage; 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'; 'use client';
import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { 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 Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { OperationID } from '@/models/oss';
import { useAppLayoutStore } from '@/stores/appLayout'; import { useAppLayoutStore } from '@/stores/appLayout';
import { information, prompts } from '@/utils/labels'; import { useModificationStore } from '@/stores/modification';
import EditorRSForm from './EditorOssCard'; import EditorRSForm from './EditorOssCard';
import EditorTermGraph from './EditorOssGraph'; import EditorTermGraph from './EditorOssGraph';
import MenuOssTabs from './MenuOssTabs'; import MenuOssTabs from './MenuOssTabs';
import { OssEditState } from './OssEditContext'; import { OssTabID, useOssEdit } from './OssEditContext';
export enum OssTabID {
CARD = 0,
GRAPH = 1
}
function OssTabs() { function OssTabs() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH; 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 hideFooter = useAppLayoutStore(state => state.hideFooter);
const { schema, loading, loadingError: errorLoading } = useOSSControl();
const [isModified, setIsModified] = useState(false); const { setIsModified } = useModificationStore();
const [selected, setSelected] = useState<OperationID[]>([]);
useBlockNavigation( useEffect(() => setIsModified(false), [setIsModified]);
isModified &&
schema !== undefined &&
!!user &&
(user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
);
useEffect(() => { useEffect(() => {
if (schema) { const oldTitle = document.title;
const oldTitle = document.title; document.title = schema.title;
document.title = schema.title; return () => {
return () => { document.title = oldTitle;
document.title = oldTitle; };
}; }, [schema.title]);
}
}, [schema, schema?.title]);
useEffect(() => { useEffect(() => {
hideFooter(activeTab === OssTabID.GRAPH); hideFooter(activeTab === OssTabID.GRAPH);
}, [activeTab, hideFooter]); }, [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) { function onSelectTab(index: number, last: number, event: Event) {
if (last === index) { if (last === index) {
return; return;
@ -93,78 +60,34 @@ function OssTabs() {
navigateTab(index); navigateTab(index);
} }
function onDestroySchema() {
if (!schema || !window.confirm(prompts.deleteOSS)) {
return;
}
deleteItem(schema.id, () => {
toast.success(information.itemDestroyed);
router.push(urls.library);
});
}
return ( return (
<OssEditState selected={selected} setSelected={setSelected}> <Tabs
{loading ? <Loader /> : null} selectedIndex={activeTab}
{errorLoading ? <ProcessError error={errorLoading} /> : null} onSelect={onSelectTab}
{schema && !loading ? ( defaultFocus
<Tabs selectedTabClassName='clr-selected'
selectedIndex={activeTab} className='flex flex-col mx-auto min-w-fit'
onSelect={onSelectTab} >
defaultFocus <Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
selectedTabClassName='clr-selected' <TabList className={clsx('w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}>
className='flex flex-col mx-auto min-w-fit' <MenuOssTabs />
>
<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} />
<TabLabel label='Карточка' title={schema.title ?? ''} /> <TabLabel label='Карточка' title={schema.title ?? ''} />
<TabLabel label='Граф' /> <TabLabel label='Граф' />
</TabList> </TabList>
</Overlay> </Overlay>
<div className='overflow-x-hidden'> <div className='overflow-x-hidden'>
<TabPanel> <TabPanel>
<EditorRSForm <EditorRSForm />
isModified={isModified} // </TabPanel>
setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
<TabPanel> <TabPanel>
<EditorTermGraph isModified={isModified} setIsModified={setIsModified} /> <EditorTermGraph />
</TabPanel> </TabPanel>
</div> </div>
</Tabs> </Tabs>
) : null}
</OssEditState>
); );
} }
export default OssTabs; 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 clsx from 'clsx';
import { useState } from 'react'; 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 useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta } from '@/models/rsform';
import { useMainHeight } from '@/stores/appLayout'; import { useMainHeight } from '@/stores/appLayout';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { information } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
import ViewConstituents from '../ViewConstituents'; import ViewConstituents from '../ViewConstituents';
@ -17,23 +23,20 @@ import ToolbarConstituenta from './ToolbarConstituenta';
// Threshold window width to switch layout. // Threshold window width to switch layout.
const SIDELIST_LAYOUT_THRESHOLD = 1000; // px const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
interface EditorConstituentaProps { function EditorConstituenta() {
activeCst?: IConstituenta;
isModified: boolean;
setIsModified: (newValue: boolean) => void;
onOpenEdit: (cstID: ConstituentaID) => void;
}
function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }: EditorConstituentaProps) {
const controller = useRSEdit(); const controller = useRSEdit();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
const showList = usePreferencesStore(state => state.showCstSideList); const showList = usePreferencesStore(state => state.showCstSideList);
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
const { cstUpdate } = useCstUpdate();
const { isModified } = useModificationStore();
const [toggleReset, setToggleReset] = useState(false); 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; const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) { 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() { function initiateSubmit() {
const element = document.getElementById(globals.constituenta_editor) as HTMLFormElement; const element = document.getElementById(globals.constituenta_editor) as HTMLFormElement;
if (element) { if (element) {
@ -76,9 +102,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
return ( return (
<> <>
<ToolbarConstituenta <ToolbarConstituenta
activeCst={activeCst} activeCst={controller.activeCst}
disabled={disabled} disabled={disabled}
modified={isModified}
onSubmit={initiateSubmit} onSubmit={initiateSubmit}
onReset={() => setToggleReset(prev => !prev)} onReset={() => setToggleReset(prev => !prev)}
/> />
@ -97,21 +122,13 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
<FormConstituenta <FormConstituenta
disabled={disabled} disabled={disabled}
id={globals.constituenta_editor} id={globals.constituenta_editor}
state={activeCst}
isModified={isModified}
toggleReset={toggleReset} toggleReset={toggleReset}
setIsModified={setIsModified} onEditTerm={handleEditTermForms}
onEditTerm={controller.editTermForms}
onRename={controller.renameCst}
onOpenEdit={onOpenEdit}
/> />
<ViewConstituents <ViewConstituents
isMounted={showList} isMounted={showList}
schema={controller.schema} expression={controller.activeCst?.definition_formal ?? ''}
expression={activeCst?.definition_formal ?? ''}
isBottom={isNarrow} isBottom={isNarrow}
activeCst={activeCst}
onOpenEdit={onOpenEdit}
/> />
</div> </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 { ICstUpdateDTO } from '@/backend/rsform/api';
import { useCstUpdate } from '@/backend/rsform/useCstUpdate'; import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons'; import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import RefsInput from '@/components/RefsInput'; import RefsInput from '@/components/RefsInput';
@ -13,27 +14,22 @@ import Indicator from '@/components/ui/Indicator';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; 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 { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
import { IExpressionParse, ParsingStatus } from '@/models/rslang'; import { IExpressionParse, ParsingStatus } from '@/models/rslang';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { errors, information, labelCstTypification } from '@/utils/labels'; import { errors, information, labelCstTypification } from '@/utils/labels';
import EditorRSExpression from '../EditorRSExpression'; import EditorRSExpression from '../EditorRSExpression';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
import ControlsOverlay from './ControlsOverlay'; import EditorControls from './EditorControls';
interface FormConstituentaProps { interface FormConstituentaProps {
disabled: boolean;
id?: string; id?: string;
state?: IConstituenta; disabled: boolean;
isModified: boolean;
toggleReset: boolean; toggleReset: boolean;
setIsModified: (newValue: boolean) => void;
onRename: () => void;
onEditTerm: () => void; onEditTerm: () => void;
onOpenEdit?: (cstID: ConstituentaID) => void; onOpenEdit?: (cstID: ConstituentaID) => void;
} }
@ -41,72 +37,68 @@ interface FormConstituentaProps {
function FormConstituenta({ function FormConstituenta({
disabled, disabled,
id, id,
state,
isModified,
setIsModified,
toggleReset, toggleReset,
onRename,
onEditTerm, onEditTerm,
onOpenEdit onOpenEdit
}: FormConstituentaProps) { }: FormConstituentaProps) {
const { cstUpdate } = useCstUpdate(); const { cstUpdate } = useCstUpdate();
const controller = useRSEdit(); const { schema, activeCst } = useRSEdit();
const schema = controller.schema; const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const [term, setTerm] = useState(state?.term_raw ?? ''); const [term, setTerm] = useState(activeCst?.term_raw ?? '');
const [textDefinition, setTextDefinition] = useState(state?.definition_raw ?? ''); const [textDefinition, setTextDefinition] = useState(activeCst?.definition_raw ?? '');
const [expression, setExpression] = useState(state?.definition_formal ?? ''); const [expression, setExpression] = useState(activeCst?.definition_formal ?? '');
const [convention, setConvention] = useState(state?.convention ?? ''); const [convention, setConvention] = useState(activeCst?.convention ?? '');
const [typification, setTypification] = useState('N/A'); const [typification, setTypification] = useState('N/A');
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined); const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
const typeInfo = state const typeInfo = activeCst
? { ? {
alias: state.alias, alias: activeCst.alias,
result: localParse ? localParse.typification : state.parse.typification, result: localParse ? localParse.typification : activeCst.parse.typification,
args: localParse ? localParse.args : state.parse.args args: localParse ? localParse.args : activeCst.parse.args
} }
: undefined; : undefined;
const [forceComment, setForceComment] = useState(false); const [forceComment, setForceComment] = useState(false);
const isBasic = !!state && isBasicConcept(state.cst_type); const isBasic = !!activeCst && isBasicConcept(activeCst.cst_type);
const isElementary = !!state && isBaseSet(state.cst_type); const isElementary = !!activeCst && isBaseSet(activeCst.cst_type);
const showConvention = !state || !!state.convention || forceComment || isBasic; const showConvention = !activeCst || !!activeCst.convention || forceComment || isBasic;
const showTypification = useDialogsStore(state => state.showShowTypeGraph); const showTypification = useDialogsStore(activeCst => activeCst.showShowTypeGraph);
useEffect(() => { useEffect(() => {
if (state) { if (activeCst) {
setConvention(state.convention); setConvention(activeCst.convention);
setTerm(state.term_raw); setTerm(activeCst.term_raw);
setTextDefinition(state.definition_raw); setTextDefinition(activeCst.definition_raw);
setExpression(state.definition_formal); setExpression(activeCst.definition_formal);
setTypification(state ? labelCstTypification(state) : 'N/A'); setTypification(activeCst ? labelCstTypification(activeCst) : 'N/A');
setForceComment(false); setForceComment(false);
setLocalParse(undefined); setLocalParse(undefined);
} }
}, [state, schema, toggleReset, setIsModified]); }, [activeCst, schema, toggleReset, setIsModified]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!state) { if (!activeCst) {
setIsModified(false); setIsModified(false);
return; return;
} }
setIsModified( setIsModified(
state.term_raw !== term || activeCst.term_raw !== term ||
state.definition_raw !== textDefinition || activeCst.definition_raw !== textDefinition ||
state.convention !== convention || activeCst.convention !== convention ||
state.definition_formal !== expression activeCst.definition_formal !== expression
); );
return () => setIsModified(false); return () => setIsModified(false);
}, [ }, [
state, activeCst,
state?.term_raw, activeCst?.term_raw,
state?.definition_formal, activeCst?.definition_formal,
state?.definition_raw, activeCst?.definition_raw,
state?.convention, activeCst?.convention,
term, term,
textDefinition, textDefinition,
expression, expression,
@ -118,23 +110,23 @@ function FormConstituenta({
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
} }
if (!state || controller.isProcessing || !schema) { if (!activeCst || isProcessing || !schema) {
return; return;
} }
const data: ICstUpdateDTO = { const data: ICstUpdateDTO = {
target: state.id, target: activeCst.id,
item_data: { item_data: {
term_raw: state.term_raw !== term ? term : undefined, term_raw: activeCst.term_raw !== term ? term : undefined,
definition_formal: state.definition_formal !== expression ? expression : undefined, definition_formal: activeCst.definition_formal !== expression ? expression : undefined,
definition_raw: state.definition_raw !== textDefinition ? textDefinition : undefined, definition_raw: activeCst.definition_raw !== textDefinition ? textDefinition : undefined,
convention: state.convention !== convention ? convention : undefined convention: activeCst.convention !== convention ? convention : undefined
} }
}; };
cstUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved)); cstUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
} }
function handleTypeGraph(event: CProps.EventMouse) { 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); toast.error(errors.typeStructureFailed);
return; return;
} }
@ -145,16 +137,7 @@ function FormConstituenta({
return ( return (
<div className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'> <div className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'>
{state ? ( {activeCst ? <EditorControls disabled={disabled} constituenta={activeCst} onEditTerm={onEditTerm} /> : null}
<ControlsOverlay
disabled={disabled}
modified={isModified}
processing={controller.isProcessing}
constituenta={state}
onEditTerm={onEditTerm}
onRename={onRename}
/>
) : null}
<form id={id} className={clsx('cc-column', 'mt-1 md:w-[48.8rem] shrink-0', 'px-6 py-1')} onSubmit={handleSubmit}> <form id={id} className={clsx('cc-column', 'mt-1 md:w-[48.8rem] shrink-0', 'px-6 py-1')} onSubmit={handleSubmit}>
<RefsInput <RefsInput
key='cst_term' key='cst_term'
@ -165,12 +148,12 @@ function FormConstituenta({
schema={schema} schema={schema}
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
value={term} value={term}
initialValue={state?.term_raw ?? ''} initialValue={activeCst?.term_raw ?? ''}
resolved={state?.term_resolved ?? 'Конституента не выбрана'} resolved={activeCst?.term_resolved ?? 'Конституента не выбрана'}
disabled={disabled} disabled={disabled}
onChange={newValue => setTerm(newValue)} onChange={newValue => setTerm(newValue)}
/> />
{state ? ( {activeCst ? (
<TextArea <TextArea
id='cst_typification' id='cst_typification'
fitContent fitContent
@ -184,24 +167,26 @@ function FormConstituenta({
colors='bg-transparent clr-text-default cursor-default' colors='bg-transparent clr-text-default cursor-default'
/> />
) : null} ) : null}
{state ? ( {activeCst ? (
<> <>
{!!state.definition_formal || !isElementary ? ( {!!activeCst.definition_formal || !isElementary ? (
<EditorRSExpression <EditorRSExpression
id='cst_expression' id='cst_expression'
label={ label={
state.cst_type === CstType.STRUCTURED activeCst.cst_type === CstType.STRUCTURED
? 'Область определения' ? 'Область определения'
: isFunctional(state.cst_type) : isFunctional(activeCst.cst_type)
? 'Определение функции' ? 'Определение функции'
: 'Формальное определение' : 'Формальное определение'
} }
placeholder={ placeholder={
state.cst_type !== CstType.STRUCTURED ? 'Родоструктурное выражение' : 'Типизация родовой структуры' activeCst.cst_type !== CstType.STRUCTURED
? 'Родоструктурное выражение'
: 'Типизация родовой структуры'
} }
value={expression} value={expression}
activeCst={state} activeCst={activeCst}
disabled={disabled || state.is_inherited} disabled={disabled || activeCst.is_inherited}
toggleReset={toggleReset} toggleReset={toggleReset}
onChangeExpression={newValue => setExpression(newValue)} onChangeExpression={newValue => setExpression(newValue)}
onChangeTypification={setTypification} onChangeTypification={setTypification}
@ -210,7 +195,7 @@ function FormConstituenta({
onShowTypeGraph={handleTypeGraph} onShowTypeGraph={handleTypeGraph}
/> />
) : null} ) : null}
{!!state.definition_raw || !isElementary ? ( {!!activeCst.definition_raw || !isElementary ? (
<RefsInput <RefsInput
id='cst_definition' id='cst_definition'
label='Текстовое определение' label='Текстовое определение'
@ -220,8 +205,8 @@ function FormConstituenta({
schema={schema} schema={schema}
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
value={textDefinition} value={textDefinition}
initialValue={state.definition_raw} initialValue={activeCst.definition_raw}
resolved={state.definition_resolved} resolved={activeCst.definition_resolved}
disabled={disabled} disabled={disabled}
onChange={newValue => setTextDefinition(newValue)} onChange={newValue => setTextDefinition(newValue)}
/> />
@ -236,12 +221,12 @@ function FormConstituenta({
label={isBasic ? 'Конвенция' : 'Комментарий'} label={isBasic ? 'Конвенция' : 'Комментарий'}
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'} placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
value={convention} value={convention}
disabled={disabled || (isBasic && state.is_inherited)} disabled={disabled || (isBasic && activeCst.is_inherited)}
onChange={event => setConvention(event.target.value)} onChange={event => setConvention(event.target.value)}
/> />
) : null} ) : null}
{!showConvention && (!disabled || controller.isProcessing) ? ( {!showConvention && (!disabled || isProcessing) ? (
<button <button
key='cst_disable_comment' key='cst_disable_comment'
id='cst_disable_comment' id='cst_disable_comment'
@ -254,7 +239,7 @@ function FormConstituenta({
</button> </button>
) : null} ) : null}
{!disabled || controller.isProcessing ? ( {!disabled || isProcessing ? (
<div className='mx-auto flex'> <div className='mx-auto flex'>
<SubmitButton <SubmitButton
key='cst_form_submit' key='cst_form_submit'
@ -264,13 +249,13 @@ function FormConstituenta({
icon={<IconSave size='1.25rem' />} icon={<IconSave size='1.25rem' />}
/> />
<Overlay position='top-[0.1rem] left-[0.4rem]' className='cc-icons'> <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 <Indicator
icon={<IconPredecessor size='1.25rem' className='text-sec-600' />} icon={<IconPredecessor size='1.25rem' className='text-sec-600' />}
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза' titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза'
/> />
) : null} ) : null}
{state.is_inherited ? ( {activeCst.is_inherited ? (
<Indicator <Indicator
icon={<IconChild size='1.25rem' className='text-sec-600' />} icon={<IconChild size='1.25rem' className='text-sec-600' />}
titleHtml='Внимание!</br> Конституента является наследником<br/>' titleHtml='Внимание!</br> Конституента является наследником<br/>'

View File

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

View File

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

View File

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

View File

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

View File

@ -3,15 +3,19 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { ILibraryUpdateDTO } from '@/backend/library/api'; import { ILibraryUpdateDTO } from '@/backend/library/api';
import { useUpdateItem } from '@/backend/library/useUpdateItem'; import { useUpdateItem } from '@/backend/library/useUpdateItem';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { IconSave } from '@/components/Icons'; import { IconSave } from '@/components/Icons';
import SelectVersion from '@/components/select/SelectVersion'; import SelectVersion from '@/components/select/SelectVersion';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; 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 { useRSEdit } from '../RSEditContext';
import ToolbarItemAccess from './ToolbarItemAccess'; import ToolbarItemAccess from './ToolbarItemAccess';
@ -19,14 +23,15 @@ import ToolbarVersioning from './ToolbarVersioning';
interface FormRSFormProps { interface FormRSFormProps {
id?: string; id?: string;
isModified: boolean;
setIsModified: (newValue: boolean) => void;
} }
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) { function FormRSForm({ id }: FormRSFormProps) {
const controller = useRSEdit(); const controller = useRSEdit();
const router = useConceptNavigation();
const schema = controller.schema; const schema = controller.schema;
const { updateItem: update } = useUpdateItem(); const { updateItem: update } = useUpdateItem();
const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const [title, setTitle] = useState(schema?.title ?? ''); const [title, setTitle] = useState(schema?.title ?? '');
const [alias, setAlias] = useState(schema?.alias ?? ''); const [alias, setAlias] = useState(schema?.alias ?? '');
@ -34,6 +39,10 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
const [visible, setVisible] = useState(schema?.visible ?? false); const [visible, setVisible] = useState(schema?.visible ?? false);
const [readOnly, setReadOnly] = useState(schema?.read_only ?? false); const [readOnly, setReadOnly] = useState(schema?.read_only ?? false);
function handleSelectVersion(version?: VersionID) {
router.push(urls.schema(schema.id, version));
}
useEffect(() => { useEffect(() => {
if (schema) { if (schema) {
setTitle(schema.title); setTitle(schema.title);
@ -127,7 +136,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
className='select-none' className='select-none'
value={schema?.version} // value={schema?.version} //
items={schema?.versions} items={schema?.versions}
onSelectValue={controller.viewVersion} onSelectValue={handleSelectVersion}
/> />
</div> </div>
</div> </div>
@ -137,14 +146,14 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
label='Описание' label='Описание'
rows={3} rows={3}
value={comment} value={comment}
disabled={!controller.isContentEditable || controller.isProcessing} disabled={!controller.isContentEditable || isProcessing}
onChange={event => setComment(event.target.value)} onChange={event => setComment(event.target.value)}
/> />
{controller.isContentEditable || isModified ? ( {controller.isContentEditable || isModified ? (
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
className='self-center mt-4' className='self-center mt-4'
loading={controller.isProcessing} loading={isProcessing}
disabled={!isModified} disabled={!isModified}
icon={<IconSave size='1.25rem' />} 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 { VisibilityIcon } from '@/components/DomainIcons';
import { IconImmutable, IconMutable } from '@/components/Icons'; import { IconImmutable, IconMutable } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
@ -10,6 +14,7 @@ import { HelpTopic } from '@/models/miscellaneous';
import { UserRole } from '@/models/user'; import { UserRole } from '@/models/user';
import { useRoleStore } from '@/stores/role'; import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { information } from '@/utils/labels';
interface ToolbarItemAccessProps { interface ToolbarItemAccessProps {
visible: boolean; visible: boolean;
@ -21,23 +26,29 @@ interface ToolbarItemAccessProps {
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) { function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) {
const role = useRoleStore(state => state.role); 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 ( return (
<Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'> <Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'>
<Label text='Доступ' className='self-center select-none' /> <Label text='Доступ' className='self-center select-none' />
<div className='ml-auto cc-icons'> <div className='ml-auto cc-icons'>
<SelectAccessPolicy <SelectAccessPolicy
disabled={role <= UserRole.EDITOR || controller.isProcessing || controller.isAttachedToOSS} disabled={role <= UserRole.EDITOR || isProcessing || controller.isAttachedToOSS}
value={policy} value={policy}
onChange={newPolicy => controller.setAccessPolicy(newPolicy)} onChange={handleSetAccessPolicy}
/> />
<MiniButton <MiniButton
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'} title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
icon={<VisibilityIcon value={visible} />} icon={<VisibilityIcon value={visible} />}
onClick={toggleVisible} onClick={toggleVisible}
disabled={role === UserRole.READER || controller.isProcessing} disabled={role === UserRole.READER || isProcessing}
/> />
<MiniButton <MiniButton
@ -50,7 +61,7 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
) )
} }
onClick={toggleReadOnly} onClick={toggleReadOnly}
disabled={role === UserRole.READER || controller.isProcessing} disabled={role === UserRole.READER || isProcessing}
/> />
<BadgeHelp topic={HelpTopic.ACCESS} className={PARAMETER.TOOLTIP_WIDTH} offset={4} /> <BadgeHelp topic={HelpTopic.ACCESS} className={PARAMETER.TOOLTIP_WIDTH} offset={4} />

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
import { IconDestroy, IconSave, IconShare } from '@/components/Icons'; import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS'; import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
@ -9,22 +10,24 @@ import { AccessPolicy, ILibraryItemEditor, LibraryItemType } from '@/models/libr
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { IRSForm } from '@/models/rsform'; import { IRSForm } from '@/models/rsform';
import { UserRole } from '@/models/user'; import { UserRole } from '@/models/user';
import { useModificationStore } from '@/stores/modification';
import { useRoleStore } from '@/stores/role'; import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { prepareTooltip, tooltips } from '@/utils/labels'; import { prepareTooltip, tooltips } from '@/utils/labels';
import { sharePage } from '@/utils/utils';
import { IRSEditContext } from '../RSEditContext'; import { IRSEditContext } from '../RSEditContext';
interface ToolbarRSFormCardProps { interface ToolbarRSFormCardProps {
modified: boolean;
onSubmit: () => void; onSubmit: () => void;
onDestroy: () => void;
controller: ILibraryItemEditor; controller: ILibraryItemEditor;
} }
function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: ToolbarRSFormCardProps) { function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardProps) {
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const canSave = modified && !controller.isProcessing; const { isModified } = useModificationStore();
const isProcessing = useIsProcessingLibrary();
const canSave = isModified && !isProcessing;
const ossSelector = (() => { const ossSelector = (() => {
if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) { if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) {
@ -37,7 +40,9 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
return ( return (
<MiniSelectorOSS <MiniSelectorOSS
items={schema.oss} 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 ( return (
<Overlay position='cc-tab-tools' className='cc-icons'> <Overlay position='cc-tab-tools' className='cc-icons'>
{ossSelector} {ossSelector}
{controller.isMutable || modified ? ( {controller.isMutable || isModified ? (
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
disabled={!canSave} disabled={!canSave}
@ -56,15 +61,15 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
<MiniButton <MiniButton
titleHtml={tooltips.shareItem(controller.schema?.access_policy)} titleHtml={tooltips.shareItem(controller.schema?.access_policy)}
icon={<IconShare size='1.25rem' className='icon-primary' />} icon={<IconShare size='1.25rem' className='icon-primary' />}
onClick={controller.share} onClick={sharePage}
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC} disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
/> />
{controller.isMutable ? ( {controller.isMutable ? (
<MiniButton <MiniButton
title='Удалить схему' title='Удалить схему'
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={!controller.isMutable || controller.isProcessing || role < UserRole.OWNER} disabled={!controller.isMutable || isProcessing || role < UserRole.OWNER}
onClick={onDestroy} onClick={controller.deleteSchema}
/> />
) : null} ) : null}
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} className={PARAMETER.TOOLTIP_WIDTH} /> <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 { IconNewVersion, IconUpload, IconVersions } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
@ -13,6 +23,45 @@ interface ToolbarVersioningProps {
function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) { function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
const controller = useRSEdit(); 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 ( return (
<Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons' layer='z-bottom'> <Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons' layer='z-bottom'>
{controller.isMutable ? ( {controller.isMutable ? (
@ -26,19 +75,19 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
: 'Переключитесь на <br/>неактуальную версию' : 'Переключитесь на <br/>неактуальную версию'
} }
disabled={controller.isContentEditable || blockReload} disabled={controller.isContentEditable || blockReload}
onClick={() => controller.restoreVersion()} onClick={handleRestoreVersion}
icon={<IconUpload size='1.25rem' className='icon-red' />} icon={<IconUpload size='1.25rem' className='icon-red' />}
/> />
<MiniButton <MiniButton
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'} titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
disabled={!controller.isContentEditable} disabled={!controller.isContentEditable}
onClick={controller.createVersion} onClick={handleCreateVersion}
icon={<IconNewVersion size='1.25rem' className='icon-green' />} icon={<IconNewVersion size='1.25rem' className='icon-green' />}
/> />
<MiniButton <MiniButton
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'} title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
disabled={!controller.schema || 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' />} 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 { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { IconCSV } from '@/components/Icons'; import { IconCSV } from '@/components/Icons';
import { type RowSelectionState } from '@/components/ui/DataTable'; import { type RowSelectionState } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
@ -20,13 +21,10 @@ import { useRSEdit } from '../RSEditContext';
import TableRSList from './TableRSList'; import TableRSList from './TableRSList';
import ToolbarRSList from './ToolbarRSList'; import ToolbarRSList from './ToolbarRSList';
interface EditorRSListProps { function EditorRSList() {
onOpenEdit: (cstID: ConstituentaID) => void;
}
function EditorRSList({ onOpenEdit }: EditorRSListProps) {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const controller = useRSEdit(); const controller = useRSEdit();
const isProcessing = useIsProcessingRSForm();
const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema?.items ?? []); const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema?.items ?? []);
const [filterText, setFilterText] = useState(''); const [filterText, setFilterText] = useState('');
@ -91,7 +89,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
controller.deselectAll(); controller.deselectAll();
return; return;
} }
if (!controller.isContentEditable || controller.isProcessing) { if (!controller.isContentEditable || isProcessing) {
return; return;
} }
if (event.key === 'Delete' && controller.canDeleteSelected) { if (event.key === 'Delete' && controller.canDeleteSelected) {
@ -170,7 +168,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
enableSelection={controller.isContentEditable} enableSelection={controller.isContentEditable}
selected={rowSelection} selected={rowSelection}
setSelected={handleRowSelection} setSelected={handleRowSelection}
onEdit={onOpenEdit} onEdit={controller.navigateCst}
onCreateNew={() => controller.createCst(undefined, false)} onCreateNew={() => controller.createCst(undefined, false)}
/> />
</div> </div>

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { import {
IconClustering, IconClustering,
IconClusteringOff, IconClusteringOff,
@ -16,6 +17,7 @@ import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS'; import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
@ -46,13 +48,24 @@ function ToolbarTermGraph({
onSaveImage onSaveImage
}: ToolbarTermGraphProps) { }: ToolbarTermGraphProps) {
const controller = useRSEdit(); 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 ( return (
<div className='cc-icons'> <div className='cc-icons'>
{controller.schema && controller.schema?.oss.length > 0 ? ( {controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS <MiniSelectorOSS
items={controller.schema.oss} 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} ) : null}
<MiniButton <MiniButton
@ -91,7 +104,7 @@ function ToolbarTermGraph({
<MiniButton <MiniButton
title='Новая конституента' title='Новая конституента'
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing} disabled={isProcessing}
onClick={onCreate} onClick={onCreate}
/> />
) : null} ) : null}
@ -99,14 +112,14 @@ function ToolbarTermGraph({
<MiniButton <MiniButton
title='Удалить выбранные' title='Удалить выбранные'
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={!controller.canDeleteSelected || controller.isProcessing} disabled={!controller.canDeleteSelected || isProcessing}
onClick={onDelete} onClick={onDelete}
/> />
) : null} ) : null}
<MiniButton <MiniButton
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />} icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
title='Граф ступеней' title='Граф ступеней'
onClick={() => controller.showTypeGraph()} onClick={handleShowTypeGraph}
/> />
<MiniButton <MiniButton
icon={<IconImage size='1.25rem' className='icon-primary' />} 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 { APP_COLORS, colorBgGraphNode } from '@/styling/color';
import { globals, PARAMETER, prefixes } from '@/utils/constants'; import { globals, PARAMETER, prefixes } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
interface ViewHiddenProps { interface ViewHiddenProps {
items: ConstituentaID[]; items: ConstituentaID[];
selected: ConstituentaID[]; selected: ConstituentaID[];
@ -23,13 +25,13 @@ interface ViewHiddenProps {
toggleSelection: (cstID: ConstituentaID) => void; toggleSelection: (cstID: ConstituentaID) => void;
setFocus: (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 windowSize = useWindowSize();
const localSelected = items.filter(id => selected.includes(id)); const localSelected = items.filter(id => selected.includes(id));
const { navigateCst } = useRSEdit();
const isFolded = useTermGraphStore(state => state.foldHidden); const isFolded = useTermGraphStore(state => state.foldHidden);
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden); const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
const setActiveCst = useTooltipsStore(state => state.setActiveCst); const setActiveCst = useTooltipsStore(state => state.setActiveCst);
@ -108,7 +110,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
: {}) : {})
}} }}
onClick={event => handleClick(cstID, event)} onClick={event => handleClick(cstID, event)}
onDoubleClick={() => onEdit(cstID)} onDoubleClick={() => navigateCst(cstID)}
data-tooltip-id={globals.constituenta_tooltip} data-tooltip-id={globals.constituenta_tooltip}
onMouseEnter={() => setActiveCst(cst)} onMouseEnter={() => setActiveCst(cst)}
> >

View File

@ -1,9 +1,18 @@
'use client'; '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 { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth'; 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 { import {
IconAdmin, IconAdmin,
IconAlert, IconAlert,
@ -19,7 +28,6 @@ import {
IconLibrary, IconLibrary,
IconMenu, IconMenu,
IconNewItem, IconNewItem,
IconNewVersion,
IconOSS, IconOSS,
IconOwner, IconOwner,
IconQR, IconQR,
@ -35,79 +43,142 @@ import Divider from '@/components/ui/Divider';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import useDropdown from '@/hooks/useDropdown'; 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 { UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { useRoleStore } from '@/stores/role'; 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'; import { useRSEdit } from './RSEditContext';
interface MenuRSTabsProps { function MenuRSTabs() {
onDestroy: () => void;
}
function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
const controller = useRSEdit(); const controller = useRSEdit();
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const oss = useGlobalOss();
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole); 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 schemaMenu = useDropdown();
const editMenu = useDropdown(); const editMenu = useDropdown();
const accessMenu = 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() { function handleDelete() {
schemaMenu.hide(); schemaMenu.hide();
onDestroy(); controller.deleteSchema();
} }
function handleDownload() { function handleDownload() {
schemaMenu.hide(); 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() { function handleUpload() {
schemaMenu.hide(); schemaMenu.hide();
controller.promptUpload(); showUpload({ itemID: controller.schema.id });
} }
function handleClone() { function handleClone() {
schemaMenu.hide(); schemaMenu.hide();
controller.promptClone(); if (isModified && !promptUnsaved()) {
return;
}
showClone({
base: controller.schema,
initialLocation: calculateCloneLocation(),
selected: controller.selected,
totalCount: controller.schema.items.length
});
} }
function handleShare() { function handleShare() {
schemaMenu.hide(); schemaMenu.hide();
controller.share(); sharePage();
} }
function handleShowQR() { function handleShowQR() {
schemaMenu.hide(); schemaMenu.hide();
controller.showQR(); showQR({ target: generatePageQR() });
}
function handleCreateVersion() {
schemaMenu.hide();
controller.createVersion();
} }
function handleReindex() { function handleReindex() {
editMenu.hide(); editMenu.hide();
controller.reindex(); resetAliases(controller.schema.id, () => toast.success(information.reindexComplete));
} }
function handleRestoreOrder() { function handleRestoreOrder() {
editMenu.hide(); editMenu.hide();
controller.reorder(); restoreOrder(controller.schema.id, () => toast.success(information.reorderComplete));
} }
function handleSubstituteCst() { function handleSubstituteCst() {
editMenu.hide(); 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() { function handleTemplates() {
@ -117,12 +188,41 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
function handleProduceStructure() { function handleProduceStructure() {
editMenu.hide(); 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() { function handleInlineSynthesis() {
editMenu.hide(); 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) { function handleChangeMode(newMode: UserRole) {
@ -155,7 +255,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
<Dropdown isOpen={schemaMenu.isOpen}> <Dropdown isOpen={schemaMenu.isOpen}>
<DropdownButton <DropdownButton
text='Поделиться' text='Поделиться'
titleHtml={tooltips.shareItem(controller.schema?.access_policy)} titleHtml={tooltips.shareItem(controller.schema.access_policy)}
icon={<IconShare size='1rem' className='icon-primary' />} icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare} onClick={handleShare}
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC} disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
@ -174,12 +274,6 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleClone} onClick={handleClone}
/> />
) : null} ) : null}
<DropdownButton
text='Сохранить версию'
disabled={!controller.isContentEditable}
onClick={handleCreateVersion}
icon={<IconNewVersion size='1rem' className='icon-green' />}
/>
<DropdownButton <DropdownButton
text='Выгрузить в Экстеор' text='Выгрузить в Экстеор'
icon={<IconDownload size='1rem' className='icon-primary' />} icon={<IconDownload size='1rem' className='icon-primary' />}
@ -189,7 +283,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
<DropdownButton <DropdownButton
text='Загрузить из Экстеор' text='Загрузить из Экстеор'
icon={<IconUpload size='1rem' className='icon-red' />} icon={<IconUpload size='1rem' className='icon-red' />}
disabled={controller.isProcessing || controller.schema?.oss.length !== 0} disabled={isProcessing || controller.schema?.oss.length !== 0}
onClick={handleUpload} onClick={handleUpload}
/> />
) : null} ) : null}
@ -197,7 +291,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
<DropdownButton <DropdownButton
text='Удалить схему' text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />} icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={controller.isProcessing || role < UserRole.OWNER} disabled={isProcessing || role < UserRole.OWNER}
onClick={handleDelete} onClick={handleDelete}
/> />
) : null} ) : null}
@ -211,11 +305,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleCreateNew} onClick={handleCreateNew}
/> />
) : null} ) : null}
{oss.schema ? ( {controller.schema.oss.length > 0 ? (
<DropdownButton <DropdownButton
text='Перейти к ОСС' text='Перейти к ОСС'
icon={<IconOSS size='1rem' className='icon-primary' />} 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} ) : null}
<DropdownButton <DropdownButton
@ -225,7 +319,6 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
/> />
</Dropdown> </Dropdown>
</div> </div>
{!controller.isArchive && user ? ( {!controller.isArchive && user ? (
<div ref={editMenu.ref}> <div ref={editMenu.ref}>
<Button <Button
@ -244,14 +337,14 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
text='Шаблоны' text='Шаблоны'
title='Создать конституенту из шаблона' title='Создать конституенту из шаблона'
icon={<IconTemplates size='1rem' className='icon-green' />} icon={<IconTemplates size='1rem' className='icon-green' />}
disabled={!controller.isContentEditable || controller.isProcessing} disabled={!controller.isContentEditable || isProcessing}
onClick={handleTemplates} onClick={handleTemplates}
/> />
<DropdownButton <DropdownButton
text='Встраивание' text='Встраивание'
titleHtml='Импортировать совокупность <br/>конституент из другой схемы' titleHtml='Импортировать совокупность <br/>конституент из другой схемы'
icon={<IconInlineSynthesis size='1rem' className='icon-green' />} icon={<IconInlineSynthesis size='1rem' className='icon-green' />}
disabled={!controller.isContentEditable || controller.isProcessing} disabled={!controller.isContentEditable || isProcessing}
onClick={handleInlineSynthesis} onClick={handleInlineSynthesis}
/> />
@ -261,21 +354,21 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
text='Упорядочить список' text='Упорядочить список'
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент' titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
icon={<IconSortList size='1rem' className='icon-primary' />} icon={<IconSortList size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || controller.isProcessing} disabled={!controller.isContentEditable || isProcessing}
onClick={handleRestoreOrder} onClick={handleRestoreOrder}
/> />
<DropdownButton <DropdownButton
text='Порядковые имена' text='Порядковые имена'
titleHtml='Присвоить порядковые имена <br/>и обновить выражения' titleHtml='Присвоить порядковые имена <br/>и обновить выражения'
icon={<IconGenerateNames size='1rem' className='icon-primary' />} icon={<IconGenerateNames size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || controller.isProcessing} disabled={!controller.isContentEditable || isProcessing}
onClick={handleReindex} onClick={handleReindex}
/> />
<DropdownButton <DropdownButton
text='Порождение структуры' text='Порождение структуры'
titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты' titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты'
icon={<IconGenerateStructure size='1rem' className='icon-primary' />} icon={<IconGenerateStructure size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || !controller.canProduceStructure || controller.isProcessing} disabled={!controller.isContentEditable || !canProduceStructure || isProcessing}
onClick={handleProduceStructure} onClick={handleProduceStructure}
/> />
<DropdownButton <DropdownButton
@ -283,11 +376,12 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
titleHtml='Заменить вхождения <br/>одной конституенты на другую' titleHtml='Заменить вхождения <br/>одной конституенты на другую'
icon={<IconReplace size='1rem' className='icon-red' />} icon={<IconReplace size='1rem' className='icon-red' />}
onClick={handleSubstituteCst} onClick={handleSubstituteCst}
disabled={!controller.isContentEditable || controller.isProcessing} disabled={!controller.isContentEditable || isProcessing}
/> />
</Dropdown> </Dropdown>
</div> </div>
) : null} ) : null}
router.push(urls.schema(itemID, version), newTab);
{controller.isArchive && user ? ( {controller.isArchive && user ? (
<Button <Button
dense dense
@ -298,10 +392,9 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
hideTitle={accessMenu.isOpen} hideTitle={accessMenu.isOpen}
className='h-full px-2' className='h-full px-2'
icon={<IconArchive size='1.25rem' className='icon-primary' />} 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} ) : null}
{user ? ( {user ? (
<div ref={accessMenu.ref}> <div ref={accessMenu.ref}>
<Button <Button

View File

@ -1,69 +1,55 @@
'use client'; 'use client';
import fileDownload from 'js-file-download';
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth'; import { useAuth } from '@/backend/auth/useAuth';
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy'; import { useDeleteItem } from '@/backend/library/useDeleteItem';
import { useSetEditors } from '@/backend/library/useSetEditors'; import { ICstCreateDTO } from '@/backend/rsform/api';
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 { useCstCreate } from '@/backend/rsform/useCstCreate'; import { useCstCreate } from '@/backend/rsform/useCstCreate';
import { useCstDelete } from '@/backend/rsform/useCstDelete'; import { useCstDelete } from '@/backend/rsform/useCstDelete';
import { useCstMove } from '@/backend/rsform/useCstMove'; 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 { useRSFormSuspense } from '@/backend/rsform/useRSForm';
import { import { ILibraryItemEditor, LibraryItemID, VersionID } from '@/models/library';
AccessPolicy, import { ConstituentaID, CstType, IConstituenta, IRSForm } from '@/models/rsform';
ILibraryItemEditor,
IVersionData,
LibraryItemID,
LocationHead,
VersionID
} from '@/models/library';
import { ICstSubstitutions } from '@/models/oss';
import { ConstituentaID, CstType, IConstituenta, IConstituentaMeta, IRSForm, TermForm } from '@/models/rsform';
import { generateAlias } from '@/models/rsformAPI'; import { generateAlias } from '@/models/rsformAPI';
import { UserID, UserRole } from '@/models/user'; import { UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { useRoleStore } from '@/stores/role'; 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 { information, prompts } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils'; 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 { export interface IRSEditContext extends ILibraryItemEditor {
schema?: IRSForm; schema: IRSForm;
selected: ConstituentaID[]; selected: ConstituentaID[];
activeCst?: IConstituenta;
isOwned: boolean; isOwned: boolean;
isArchive: boolean; isArchive: boolean;
isMutable: boolean; isMutable: boolean;
isContentEditable: boolean; isContentEditable: boolean;
isProcessing: boolean;
isAttachedToOSS: boolean; isAttachedToOSS: boolean;
canProduceStructure: boolean;
canDeleteSelected: boolean; canDeleteSelected: boolean;
setOwner: (newOwner: UserID) => void; navigateRSForm: ({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void; navigateCst: (cstID: ConstituentaID) => void;
promptEditors: () => void; navigateOss: (target: LibraryItemID, newTab?: boolean) => void;
promptLocation: () => void;
deleteSchema: () => void;
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>; setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
select: (target: ConstituentaID) => void; select: (target: ConstituentaID) => void;
@ -71,35 +57,12 @@ export interface IRSEditContext extends ILibraryItemEditor {
toggleSelect: (target: ConstituentaID) => void; toggleSelect: (target: ConstituentaID) => void;
deselectAll: () => 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; moveUp: () => void;
moveDown: () => void; moveDown: () => void;
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void; createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
renameCst: () => void;
cloneCst: () => void; cloneCst: () => void;
promptDeleteCst: () => void; promptDeleteCst: () => void;
editTermForms: () => void;
promptTemplate: () => 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); const RSEditContext = createContext<IRSEditContext | null>(null);
@ -113,28 +76,11 @@ export const useRSEdit = () => {
interface RSEditStateProps { interface RSEditStateProps {
itemID: LibraryItemID; itemID: LibraryItemID;
activeTab: RSTabID;
versionID?: VersionID; 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 = ({ export const RSEditState = ({ itemID, versionID, activeTab, children }: React.PropsWithChildren<RSEditStateProps>) => {
itemID,
versionID,
selected,
setSelected,
activeCst,
isModified,
onCreateCst,
onDeleteCst,
children
}: React.PropsWithChildren<RSEditStateProps>) => {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const adminMode = usePreferencesStore(state => state.adminMode); const adminMode = usePreferencesStore(state => state.adminMode);
@ -142,72 +88,33 @@ export const RSEditState = ({
const adjustRole = useRoleStore(state => state.adjustRole); const adjustRole = useRoleStore(state => state.adjustRole);
const { schema } = useRSFormSuspense({ itemID: itemID, version: versionID }); const { schema } = useRSFormSuspense({ itemID: itemID, version: versionID });
const isProcessing = useIsProcessingRSForm(); const { isModified } = useModificationStore();
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 isOwned = user?.id === schema?.owner || false; const isOwned = user?.id === schema?.owner || false;
const isArchive = !!versionID; const isArchive = !!versionID;
const isMutable = role > UserRole.READER && !schema.read_only;
const isMutable = role > UserRole.READER && !schema?.read_only;
const isContentEditable = isMutable && !isArchive; const isContentEditable = isMutable && !isArchive;
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema?.cstByID.get(id)?.is_inherited); const isAttachedToOSS = schema.oss.length > 0;
const isAttachedToOSS =
!!schema && schema.oss.length > 0 && (schema.stats.count_inherited > 0 || schema.items.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 activeCst: IConstituenta | undefined = (() => {
const showCreateVersion = useDialogsStore(state => state.showCreateVersion); if (!schema || selected.length === 0) {
const showEditVersions = useDialogsStore(state => state.showEditVersions); return undefined;
const showEditEditors = useDialogsStore(state => state.showEditEditors); } else {
const showEditLocation = useDialogsStore(state => state.showChangeLocation); 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 showCreateCst = useDialogsStore(state => state.showCreateCst);
const showDeleteCst = useDialogsStore(state => state.showDeleteCst); 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 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( useEffect(
() => () =>
@ -220,79 +127,78 @@ export const RSEditState = ({
[schema, adjustRole, isOwned, user, adminMode] [schema, adjustRole, isOwned, user, adminMode]
); );
function viewVersion(version?: VersionID, newTab?: boolean) { function navigateOss(target: LibraryItemID, 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) {
router.push(urls.oss(target), newTab); router.push(urls.oss(target), newTab);
} }
function restoreVersion() { function navigateRSForm({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) {
if (!versionID || !window.confirm(prompts.restoreArchive)) { if (!schema) {
return; return;
} }
model.versionRestore(versionID, () => { const data = {
toast.success(information.versionRestored); id: schema.id,
viewVersion(undefined); 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) { function handleCreateCst(data: ICstCreateDTO) {
if (!schema) {
return;
}
data.alias = data.alias || generateAlias(data.cst_type, schema); data.alias = data.alias || generateAlias(data.cst_type, schema);
cstCreate({ itemID: itemID, data }, newCst => { cstCreate({ itemID: itemID, data }, newCst => {
toast.success(information.newConstituent(newCst.alias)); toast.success(information.newConstituent(newCst.alias));
setSelected([newCst.id]); setSelected([newCst.id]);
if (onCreateCst) onCreateCst(newCst); navigateRSForm({ tab: activeTab, activeID: newCst.id });
}); if (activeTab === RSTabID.CST_LIST) {
} setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
function handleRenameCst(data: ICstRenameDTO) { if (element) {
const oldAlias = renameInitialData?.alias ?? ''; element.scrollIntoView({
cstRename({ itemID: itemID, data }, () => toast.success(information.renameComplete(oldAlias, data.alias))); behavior: 'smooth',
} block: 'nearest',
inline: 'end'
function handleSubstituteCst(data: ICstSubstitutions) { });
cstSubstitute({ itemID: itemID, data }, () => { }
setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id))); }, PARAMETER.refreshTimeout);
toast.success(information.substituteSingle); }
}); });
} }
function handleDeleteCst(deleted: ConstituentaID[]) { function handleDeleteCst(deleted: ConstituentaID[]) {
if (!schema) {
return;
}
const data = { const data = {
items: deleted items: deleted
}; };
@ -304,88 +210,18 @@ export const RSEditState = ({
cstDelete({ itemID: itemID, data }, () => { cstDelete({ itemID: itemID, data }, () => {
toast.success(information.constituentsDestroyed(deletedNames)); toast.success(information.constituentsDestroyed(deletedNames));
setSelected(nextActive ? [nextActive] : []); setSelected(nextActive ? [nextActive] : []);
onDeleteCst?.(nextActive); if (!nextActive) {
}); navigateRSForm({ tab: RSTabID.CST_LIST });
} } else if (activeTab === RSTabID.CST_EDIT) {
navigateRSForm({ tab: activeTab, activeID: nextActive });
function handleSaveWordforms(forms: TermForm[]) { } else {
if (!activeCst) { navigateRSForm({ tab: activeTab });
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);
} }
}); });
} }
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() { function moveUp() {
if (!schema?.items || selected.length === 0) { if (!schema.items || selected.length === 0) {
return; return;
} }
const currentIndex = schema.items.reduce((prev, cst, index) => { const currentIndex = schema.items.reduce((prev, cst, index) => {
@ -407,7 +243,7 @@ export const RSEditState = ({
} }
function moveDown() { function moveDown() {
if (!schema?.items || selected.length === 0) { if (!schema.items || selected.length === 0) {
return; return;
} }
let count = 0; let count = 0;
@ -433,9 +269,6 @@ export const RSEditState = ({
} }
function createCst(type: CstType | undefined, skipDialog: boolean, definition?: string) { function createCst(type: CstType | undefined, skipDialog: boolean, definition?: string) {
if (!schema) {
return;
}
const targetType = type ?? activeCst?.cst_type ?? CstType.BASE; const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
const data: ICstCreateDTO = { const data: ICstCreateDTO = {
insert_after: activeCst?.id ?? null, insert_after: activeCst?.id ?? null,
@ -455,7 +288,7 @@ export const RSEditState = ({
} }
function cloneCst() { function cloneCst() {
if (!activeCst || !schema) { if (!activeCst) {
return; return;
} }
const data: ICstCreateDTO = { const data: ICstCreateDTO = {
@ -471,213 +304,50 @@ export const RSEditState = ({
handleCreateCst(data); 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() { function promptDeleteCst() {
if (!schema) {
return;
}
showDeleteCst({ schema: schema, selected: selected, onDelete: handleDeleteCst }); 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() { function promptTemplate() {
if ((isModified && !promptUnsaved()) || !schema) { if (isModified && !promptUnsaved()) {
return; return;
} }
showCstTemplate({ schema: schema, onCreate: handleCreateCst, insertAfter: activeCst?.id }); 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 ( return (
<RSEditContext <RSEditContext
value={{ value={{
schema: schema, schema,
selected, selected,
activeCst,
isOwned, isOwned,
isArchive, isArchive,
isMutable, isMutable,
isContentEditable, isContentEditable,
isProcessing,
isAttachedToOSS, isAttachedToOSS,
canProduceStructure,
canDeleteSelected, canDeleteSelected,
setOwner, navigateRSForm,
setAccessPolicy, navigateCst,
promptEditors,
promptLocation,
setSelected: setSelected, deleteSchema,
navigateOss,
setSelected,
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]), select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)), deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
toggleSelect: (target: ConstituentaID) => toggleSelect: (target: ConstituentaID) =>
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])), setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
deselectAll: () => setSelected([]), deselectAll: () => setSelected([]),
viewOSS,
viewVersion,
viewPredecessor,
createVersion,
restoreVersion,
promptEditVersions,
moveUp, moveUp,
moveDown, moveDown,
createCst, createCst,
cloneCst, cloneCst,
renameCst,
promptDeleteCst, promptDeleteCst,
editTermForms,
promptTemplate, promptTemplate
promptClone,
promptUpload: () => showUpload({ itemID: model.itemID! }),
download,
share,
reindex,
reorder,
inlineSynthesis,
produceStructure,
substitute,
showTypeGraph: () => showTypeGraph({ items: typeInfo }),
showQR: () => showQR({ target: generateQR() })
}} }}
> >
{children} {children}

View File

@ -1,21 +1,81 @@
'use client'; 'use client';
import axios from 'axios';
import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router'; 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 useQueryStrings from '@/hooks/useQueryStrings';
import { LibraryItemID } from '@/models/library';
import { useModificationStore } from '@/stores/modification';
import { RSEditState, RSTabID } from './RSEditContext';
import RSTabs from './RSTabs'; import RSTabs from './RSTabs';
function RSFormPage() { function RSFormPage() {
const params = useParams(); const router = useConceptNavigation();
const query = useQueryStrings(); 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 ( return (
<RSFormState itemID={params.id ?? ''} versionID={version}> <ErrorBoundary
<RSTabs /> FallbackComponent={({ error }) => (
</RSFormState> <ProcessError error={error as ErrorData} isArchive={!!version} itemID={itemID} />
)}
>
<RSEditState itemID={itemID} versionID={version} activeTab={activeTab}>
<RSTabs />
</RSEditState>
</ErrorBoundary>
); );
} }
export default RSFormPage; 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'; 'use client';
import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import { useParams } from 'react-router';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify';
import { useGlobalOss } from '@/app/GlobalOssContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
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 Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { LibraryItemID } from '@/models/library';
import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsform';
import { useAppLayoutStore } from '@/stores/appLayout'; import { useAppLayoutStore } from '@/stores/appLayout';
import { PARAMETER, prefixes } from '@/utils/constants'; import { useModificationStore } from '@/stores/modification';
import { information, labelVersion, prompts } from '@/utils/labels'; import { labelVersion } from '@/utils/labels';
import { OssTabID } from '../OssPage/OssTabs';
import EditorConstituenta from './EditorConstituenta'; import EditorConstituenta from './EditorConstituenta';
import EditorRSForm from './EditorRSFormCard'; import EditorRSForm from './EditorRSFormCard';
import EditorRSList from './EditorRSList'; import EditorRSList from './EditorRSList';
import EditorTermGraph from './EditorTermGraph'; import EditorTermGraph from './EditorTermGraph';
import MenuRSTabs from './MenuRSTabs'; import MenuRSTabs from './MenuRSTabs';
import { RSEditState } from './RSEditContext'; import { RSTabID, useRSEdit } from './RSEditContext';
export enum RSTabID {
CARD = 0,
CST_LIST = 1,
CST_EDIT = 2,
TERM_GRAPH = 3
}
function RSTabs() { function RSTabs() {
const params = useParams();
const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const router = useConceptNavigation();
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD; 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 cstQuery = query.get('active');
const hideFooter = useAppLayoutStore(state => state.hideFooter); const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { schema, loading, errorLoading, isArchive } = useRSFormControl(); const { setIsModified } = useModificationStore();
const { deleteItem } = useDeleteItem(); const { schema, selected, setSelected, navigateRSForm } = useRSEdit();
const oss = useGlobalOss();
const [isModified, setIsModified] = useState(false); useEffect(() => setIsModified(false), [setIsModified]);
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(() => { useEffect(() => {
if (schema) { const oldTitle = document.title;
const oldTitle = document.title; document.title = schema.title;
document.title = schema.title; return () => {
return () => { document.title = oldTitle;
document.title = oldTitle; };
}; }, [schema?.title]);
}
}, [schema, schema?.title]);
useEffect(() => { useEffect(() => {
hideFooter(activeTab !== RSTabID.CARD); hideFooter(activeTab !== RSTabID.CARD);
@ -89,30 +53,6 @@ function RSTabs() {
return () => hideFooter(false); return () => hideFooter(false);
}, [activeTab, cstQuery, setSelected, schema, hideFooter, setIsModified]); }, [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) { function onSelectTab(index: number, last: number, event: Event) {
if (last === index) { if (last === index) {
return; return;
@ -129,160 +69,54 @@ function RSTabs() {
} }
} }
} }
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined); navigateRSForm({ tab: index, activeID: 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);
}
});
} }
return ( return (
<RSEditState <>
selected={selected} <Tabs
setSelected={setSelected} selectedIndex={activeTab}
activeCst={activeCst} onSelect={onSelectTab}
isModified={isModified} defaultFocus
onCreateCst={onCreateCst} selectedTabClassName='clr-selected'
onDeleteCst={onDeleteCst} className='flex flex-col mx-auto min-w-fit'
> >
{loading ? <Loader /> : null} <Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
{errorLoading ? <ProcessError error={errorLoading} isArchive={isArchive} itemID={itemID} /> : null} <TabList
{schema && !loading ? ( className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}
<Tabs >
selectedIndex={activeTab} <MenuRSTabs />
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} />
<TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} /> <TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} />
<TabLabel <TabLabel
label='Содержание' label='Содержание'
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${ titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${schema.stats?.count_errors ?? 0}`}
schema.stats?.count_errors ?? 0 />
}`} <TabLabel label='Редактор' />
/> <TabLabel label='Граф термов' />
<TabLabel label='Редактор' /> </TabList>
<TabLabel label='Граф термов' /> </Overlay>
</TabList>
</Overlay>
<div className='overflow-x-hidden'> <div className='overflow-x-hidden'>
<TabPanel> <TabPanel>
<EditorRSForm <EditorRSForm />
isModified={isModified} // </TabPanel>
setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
<TabPanel> <TabPanel>
<EditorRSList onOpenEdit={onOpenCst} /> <EditorRSList />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<EditorConstituenta <EditorConstituenta />
isModified={isModified} </TabPanel>
setIsModified={setIsModified}
activeCst={activeCst}
onOpenEdit={onOpenCst}
/>
</TabPanel>
<TabPanel> <TabPanel>
<EditorTermGraph onOpenEdit={onOpenCst} /> <EditorTermGraph />
</TabPanel> </TabPanel>
</div> </div>
</Tabs> </Tabs>
) : null} </>
</RSEditState>
); );
} }
export default RSTabs; 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 { useState } from 'react';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform'; import { IConstituenta } from '@/models/rsform';
import { UserRole } from '@/models/user'; import { UserRole } from '@/models/user';
import { useFitHeight } from '@/stores/appLayout'; import { useFitHeight } from '@/stores/appLayout';
import { useRoleStore } from '@/stores/role'; import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
import ConstituentsSearch from './ConstituentsSearch'; import ConstituentsSearch from './ConstituentsSearch';
import TableSideConstituents from './TableSideConstituents'; import TableSideConstituents from './TableSideConstituents';
@ -19,16 +20,14 @@ const COLUMN_DENSE_SEARCH_THRESHOLD = 1100;
interface ViewConstituentsProps { interface ViewConstituentsProps {
expression: string; expression: string;
isBottom?: boolean; isBottom?: boolean;
activeCst?: IConstituenta;
schema?: IRSForm;
onOpenEdit: (cstID: ConstituentaID) => void;
isMounted: boolean; isMounted: boolean;
} }
function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit, isMounted }: ViewConstituentsProps) { function ViewConstituents({ expression, isBottom, isMounted }: ViewConstituentsProps) {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const listHeight = useFitHeight(!isBottom ? '8.2rem' : role !== UserRole.READER ? '42rem' : '35rem', '10rem'); const listHeight = useFitHeight(!isBottom ? '8.2rem' : role !== UserRole.READER ? '42rem' : '35rem', '10rem');
const { schema, activeCst, navigateCst } = useRSEdit();
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []); const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
@ -60,7 +59,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
maxHeight={listHeight} maxHeight={listHeight}
items={filteredData} items={filteredData}
activeCst={activeCst} activeCst={activeCst}
onOpenEdit={onOpenEdit} onOpenEdit={navigateCst}
autoScroll={!isBottom} autoScroll={!isBottom}
/> />
</div> </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 axios, { AxiosError, AxiosHeaderValue, AxiosResponse } from 'axios';
import { toast } from 'react-toastify';
import { AliasMapping } from '@/models/rslang'; import { AliasMapping } from '@/models/rslang';
import { prompts } from './labels'; import { information, prompts } from './labels';
/** /**
* Checks if arguments is Node. * 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;' }); 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);
}