F: Implement react-query pt4
This commit is contained in:
parent
6543d88cbe
commit
519b5f6634
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -6,7 +6,6 @@ import { ErrorBoundary } from 'react-error-boundary';
|
|||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { queryClient } from '@/backend/queryClient';
|
||||
import { GlobalOssState } from '@/app/GlobalOssContext';
|
||||
|
||||
import ErrorFallback from './ErrorFallback';
|
||||
|
||||
|
@ -31,12 +30,10 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
|
|||
>
|
||||
<IntlProvider locale='ru' defaultLocale='ru'>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GlobalOssState>
|
||||
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{children}
|
||||
|
||||
</GlobalOssState>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
</ErrorBoundary>);
|
||||
|
|
|
@ -2,10 +2,13 @@ import { queryOptions } from '@tanstack/react-query';
|
|||
|
||||
import { axiosInstance } from '@/backend/axiosInstance';
|
||||
import { DELAYS } from '@/backend/configuration';
|
||||
import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, VersionID } from '@/models/library';
|
||||
import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, LibraryItemType, VersionID } from '@/models/library';
|
||||
import { ConstituentaID, IRSFormData } from '@/models/rsform';
|
||||
import { UserID } from '@/models/user';
|
||||
|
||||
import { ossApi } from '../oss/api';
|
||||
import { rsformsApi } from '../rsform/api';
|
||||
|
||||
/**
|
||||
* Represents update data for renaming Location.
|
||||
*/
|
||||
|
@ -67,6 +70,11 @@ export const libraryApi = {
|
|||
})
|
||||
.then(response => response.data)
|
||||
}),
|
||||
getItemQueryOptions: ({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) => {
|
||||
return itemType === LibraryItemType.RSFORM
|
||||
? rsformsApi.getRSFormQueryOptions({ itemID })
|
||||
: ossApi.getOssQueryOptions({ itemID });
|
||||
},
|
||||
getTemplatesQueryOptions: () =>
|
||||
queryOptions({
|
||||
queryKey: [libraryApi.baseKey, 'templates'],
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { useIsMutating } from '@tanstack/react-query';
|
||||
|
||||
import { ossApi } from '../oss/api';
|
||||
import { rsformsApi } from '../rsform/api';
|
||||
import { libraryApi } from './api';
|
||||
|
||||
export const useIsProcessingLibrary = () => {
|
||||
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] });
|
||||
return countMutations !== 0;
|
||||
const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] });
|
||||
const countRSForm = useIsMutating({ mutationKey: [rsformsApi.baseKey] });
|
||||
return countMutations + countOss + countRSForm !== 0;
|
||||
};
|
||||
|
|
23
rsconcept/frontend/src/backend/library/useLibraryItem.tsx
Normal file
23
rsconcept/frontend/src/backend/library/useLibraryItem.tsx
Normal 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)
|
||||
};
|
||||
}
|
|
@ -92,7 +92,7 @@ export interface ICstRelocateDTO {
|
|||
}
|
||||
|
||||
export const ossApi = {
|
||||
baseKey: 'library',
|
||||
baseKey: 'oss',
|
||||
|
||||
getOssQueryOptions: ({ itemID }: { itemID?: LibraryItemID }) => {
|
||||
return queryOptions({
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
|
||||
import { OssLoader } from '@/models/OssLoader';
|
||||
|
||||
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
|
||||
import { ossApi } from './api';
|
||||
|
||||
export function useOss({ itemID }: { itemID?: LibraryItemID }) {
|
||||
|
@ -17,12 +17,12 @@ export function useOss({ itemID }: { itemID?: LibraryItemID }) {
|
|||
return { schema: schema, isLoading: isLoading || libraryLoading, error: error };
|
||||
}
|
||||
|
||||
export function useOssSuspense({ itemID }: { itemID?: LibraryItemID }) {
|
||||
export function useOssSuspense({ itemID }: { itemID: LibraryItemID }) {
|
||||
const { items: libraryItems } = useLibrarySuspense();
|
||||
const { data } = useSuspenseQuery({
|
||||
...ossApi.getOssQueryOptions({ itemID })
|
||||
});
|
||||
const schema = data ? new OssLoader(data, libraryItems).produceOSS() : undefined;
|
||||
const schema = new OssLoader(data!, libraryItems).produceOSS();
|
||||
return { schema };
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ export const useUpdatePositions = () => {
|
|||
onSuccess: (_, variables) => updateTimestamp(variables.itemID)
|
||||
});
|
||||
return {
|
||||
cstDelete: (
|
||||
updatePositions: (
|
||||
data: {
|
||||
itemID: LibraryItemID; //
|
||||
positions: IOperationPosition[];
|
||||
|
|
|
@ -107,7 +107,7 @@ export interface ICheckConstituentaDTO {
|
|||
}
|
||||
|
||||
export const rsformsApi = {
|
||||
baseKey: 'library',
|
||||
baseKey: 'rsform',
|
||||
|
||||
getRSFormQueryOptions: ({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) => {
|
||||
return queryOptions({
|
||||
|
|
|
@ -15,11 +15,11 @@ export function useRSForm({ itemID, version }: { itemID?: LibraryItemID; version
|
|||
return { schema, isLoading, error };
|
||||
}
|
||||
|
||||
export function useRSFormSuspense({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) {
|
||||
export function useRSFormSuspense({ itemID, version }: { itemID: LibraryItemID; version?: VersionID }) {
|
||||
const { data } = useSuspenseQuery({
|
||||
...rsformsApi.getRSFormQueryOptions({ itemID, version })
|
||||
});
|
||||
const schema = data ? new RSFormLoader(data).produceRSForm() : undefined;
|
||||
const schema = new RSFormLoader(data!).produceRSForm();
|
||||
return { schema };
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import clsx from 'clsx';
|
|||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { IRSFormCloneDTO } from '@/backend/library/api';
|
||||
|
@ -18,7 +19,6 @@ import MiniButton from '@/components/ui/MiniButton';
|
|||
import Modal from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
|
||||
import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||
import { ConstituentaID } from '@/models/rsform';
|
||||
|
|
|
@ -1,34 +1,51 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
|
||||
import { useVersionDelete } from '@/backend/library/useVersionDelete';
|
||||
import { useVersionUpdate } from '@/backend/library/useVersionUpdate';
|
||||
import { IconReset, IconSave } from '@/components/Icons';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { IVersionData, IVersionInfo, VersionID } from '@/models/library';
|
||||
import { ILibraryItemVersioned, IVersionData, IVersionInfo, VersionID } from '@/models/library';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { information } from '@/utils/labels';
|
||||
|
||||
import TableVersions from './TableVersions';
|
||||
|
||||
export interface DlgEditVersionsProps {
|
||||
versions: IVersionInfo[];
|
||||
onDelete: (versionID: VersionID) => void;
|
||||
onUpdate: (versionID: VersionID, data: IVersionData) => void;
|
||||
item: ILibraryItemVersioned;
|
||||
}
|
||||
|
||||
function DlgEditVersions() {
|
||||
const { versions, onDelete, onUpdate } = useDialogsStore(state => state.props as DlgEditVersionsProps);
|
||||
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined);
|
||||
const processing = false; // TODO: fix processing hook and versions update
|
||||
const { item } = useDialogsStore(state => state.props as DlgEditVersionsProps);
|
||||
const router = useConceptNavigation();
|
||||
const processing = useIsProcessingLibrary();
|
||||
const { versionDelete } = useVersionDelete();
|
||||
const { versionUpdate } = useVersionUpdate();
|
||||
|
||||
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined);
|
||||
const [version, setVersion] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
const isValid = selected && versions.every(ver => ver.id === selected.id || ver.version != version);
|
||||
const isValid = selected && item.versions.every(ver => ver.id === selected.id || ver.version != version);
|
||||
const isModified = selected && (selected.version != version || selected.description != description);
|
||||
|
||||
function handleDeleteVersion(versionID: VersionID) {
|
||||
versionDelete({ itemID: item.id, versionID: versionID }, () => {
|
||||
toast.success(information.versionDestroyed);
|
||||
if (versionID === versionID) {
|
||||
router.push(urls.schema(item.id));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleUpdate() {
|
||||
if (!isModified || !selected || processing || !isValid) {
|
||||
return;
|
||||
|
@ -37,7 +54,14 @@ function DlgEditVersions() {
|
|||
version: version,
|
||||
description: description
|
||||
};
|
||||
onUpdate(selected.id, data);
|
||||
versionUpdate(
|
||||
{
|
||||
itemID: item.id, //
|
||||
versionID: selected.id,
|
||||
data: data
|
||||
},
|
||||
() => toast.success(information.changesSaved)
|
||||
);
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
|
@ -57,9 +81,9 @@ function DlgEditVersions() {
|
|||
<Modal readonly header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
|
||||
<TableVersions
|
||||
processing={processing}
|
||||
items={versions}
|
||||
onDelete={onDelete}
|
||||
onSelect={versionID => setSelected(versions.find(ver => ver.id === versionID))}
|
||||
items={item.versions}
|
||||
onDelete={handleDeleteVersion}
|
||||
onSelect={versionID => setSelected(item.versions.find(ver => ver.id === versionID))}
|
||||
selected={selected?.id}
|
||||
/>
|
||||
|
||||
|
|
|
@ -100,16 +100,9 @@ export interface ILibraryItemVersioned extends ILibraryItemData {
|
|||
* Represents common {@link ILibraryItem} editor controller.
|
||||
*/
|
||||
export interface ILibraryItemEditor {
|
||||
schema?: ILibraryItemData;
|
||||
schema: ILibraryItemData;
|
||||
deleteSchema: () => void;
|
||||
|
||||
isMutable: boolean;
|
||||
isProcessing: boolean;
|
||||
isAttachedToOSS: boolean;
|
||||
|
||||
setOwner: (newOwner: UserID) => void;
|
||||
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
||||
promptEditors: () => void;
|
||||
promptLocation: () => void;
|
||||
|
||||
share: () => void;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
function HomePage() {
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
IconEdit2,
|
||||
IconEditor,
|
||||
IconMenu,
|
||||
IconNewVersion,
|
||||
IconOwner,
|
||||
IconReader,
|
||||
IconShare,
|
||||
|
@ -54,9 +53,6 @@ function HelpRSMenu() {
|
|||
<li>
|
||||
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
||||
</li>
|
||||
<li>
|
||||
<IconNewVersion size='1.25rem' className='inline-icon icon-green' /> Сохранить версию
|
||||
</li>
|
||||
<li>
|
||||
<IconDownload className='inline-icon' /> Выгрузить – сохранить в файле формата Экстеор
|
||||
</li>
|
||||
|
|
|
@ -3,22 +3,19 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import FlexColumn from '@/components/ui/FlexColumn';
|
||||
import { LibraryItemType } from '@/models/library';
|
||||
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
|
||||
import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
import FormOSS from './FormOSS';
|
||||
import OssStats from './OssStats';
|
||||
|
||||
interface EditorOssCardProps {
|
||||
isModified: boolean;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
onDestroy: () => void;
|
||||
}
|
||||
|
||||
function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardProps) {
|
||||
function EditorOssCard() {
|
||||
const controller = useOssEdit();
|
||||
const { isModified } = useModificationStore();
|
||||
|
||||
function initiateSubmit() {
|
||||
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
|
||||
|
@ -38,12 +35,7 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
|
|||
|
||||
return (
|
||||
<>
|
||||
<ToolbarRSFormCard
|
||||
modified={isModified}
|
||||
onSubmit={initiateSubmit}
|
||||
onDestroy={onDestroy}
|
||||
controller={controller}
|
||||
/>
|
||||
<ToolbarRSFormCard onSubmit={initiateSubmit} controller={controller} />
|
||||
<div
|
||||
onKeyDown={handleInput}
|
||||
className={clsx(
|
||||
|
@ -54,8 +46,8 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
|
|||
)}
|
||||
>
|
||||
<FlexColumn className='px-3'>
|
||||
<FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
||||
<EditorLibraryItem item={controller.schema} isModified={isModified} controller={controller} />
|
||||
<FormOSS id={globals.library_item_editor} />
|
||||
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.OSS} controller={controller} />
|
||||
</FlexColumn>
|
||||
|
||||
{controller.schema ? <OssStats stats={controller.schema.stats} /> : null}
|
||||
|
|
|
@ -6,25 +6,27 @@ import { toast } from 'react-toastify';
|
|||
|
||||
import { ILibraryUpdateDTO } from '@/backend/library/api';
|
||||
import { useUpdateItem } from '@/backend/library/useUpdateItem';
|
||||
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||
import { IconSave } from '@/components/Icons';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { LibraryItemType } from '@/models/library';
|
||||
import ToolbarItemAccess from '@/pages/RSFormPage/EditorRSFormCard/ToolbarItemAccess';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { information } from '@/utils/labels';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
|
||||
interface FormOSSProps {
|
||||
id?: string;
|
||||
isModified: boolean;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
|
||||
function FormOSS({ id }: FormOSSProps) {
|
||||
const { updateItem: update } = useUpdateItem();
|
||||
const controller = useOssEdit();
|
||||
const { isModified, setIsModified } = useModificationStore();
|
||||
const isProcessing = useIsProcessingOss();
|
||||
const schema = controller.schema;
|
||||
|
||||
const [title, setTitle] = useState(schema?.title ?? '');
|
||||
|
@ -125,14 +127,14 @@ function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
|
|||
label='Описание'
|
||||
rows={3}
|
||||
value={comment}
|
||||
disabled={!controller.isMutable || controller.isProcessing}
|
||||
disabled={!controller.isMutable || isProcessing}
|
||||
onChange={event => setComment(event.target.value)}
|
||||
/>
|
||||
{controller.isMutable || isModified ? (
|
||||
<SubmitButton
|
||||
text='Сохранить изменения'
|
||||
className='self-center mt-4'
|
||||
loading={controller.isProcessing}
|
||||
loading={isProcessing}
|
||||
disabled={!isModified}
|
||||
icon={<IconSave size='1.25rem' />}
|
||||
/>
|
||||
|
|
|
@ -4,15 +4,10 @@ import { ReactFlowProvider } from 'reactflow';
|
|||
|
||||
import OssFlow from './OssFlow';
|
||||
|
||||
interface EditorOssGraphProps {
|
||||
isModified: boolean;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
|
||||
function EditorOssGraph() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<OssFlow isModified={isModified} setIsModified={setIsModified} />
|
||||
<OssFlow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||
import {
|
||||
IconChild,
|
||||
IconConnect,
|
||||
|
@ -49,6 +50,8 @@ function NodeContextMenu({
|
|||
onRelocateConstituents
|
||||
}: NodeContextMenuProps) {
|
||||
const controller = useOssEdit();
|
||||
const isProcessing = useIsProcessingOss();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const readyForSynthesis = (() => {
|
||||
|
@ -64,7 +67,7 @@ function NodeContextMenu({
|
|||
return false;
|
||||
}
|
||||
|
||||
const argumentOperations = argumentIDs.map(id => controller.schema!.operationByID.get(id)!);
|
||||
const argumentOperations = argumentIDs.map(id => controller.schema.operationByID.get(id)!);
|
||||
if (argumentOperations.some(item => item.result === null)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -82,7 +85,7 @@ function NodeContextMenu({
|
|||
useEffect(() => setIsOpen(true), []);
|
||||
|
||||
const handleOpenSchema = () => {
|
||||
controller.openOperationSchema(operation.id);
|
||||
controller.navigateOperationSchema(operation.id);
|
||||
};
|
||||
|
||||
const handleEditSchema = () => {
|
||||
|
@ -126,7 +129,7 @@ function NodeContextMenu({
|
|||
text='Редактировать'
|
||||
title='Редактировать операцию'
|
||||
icon={<IconEdit2 size='1rem' className='icon-primary' />}
|
||||
disabled={!controller.isMutable || controller.isProcessing}
|
||||
disabled={!controller.isMutable || isProcessing}
|
||||
onClick={handleEditOperation}
|
||||
/>
|
||||
|
||||
|
@ -135,7 +138,7 @@ function NodeContextMenu({
|
|||
text='Открыть схему'
|
||||
titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')}
|
||||
icon={<IconRSForm size='1rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={isProcessing}
|
||||
onClick={handleOpenSchema}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -144,7 +147,7 @@ function NodeContextMenu({
|
|||
text='Создать схему'
|
||||
title='Создать пустую схему для загрузки'
|
||||
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={isProcessing}
|
||||
onClick={handleCreateSchema}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -153,7 +156,7 @@ function NodeContextMenu({
|
|||
text={!operation.result ? 'Загрузить схему' : 'Изменить схему'}
|
||||
title='Выбрать схему для загрузки'
|
||||
icon={<IconConnect size='1rem' className='icon-primary' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={isProcessing}
|
||||
onClick={handleEditSchema}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -166,7 +169,7 @@ function NodeContextMenu({
|
|||
: 'Необходимо предоставить все аргументы'
|
||||
}
|
||||
icon={<IconExecute size='1rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing || !readyForSynthesis}
|
||||
disabled={isProcessing || !readyForSynthesis}
|
||||
onClick={handleRunSynthesis}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -176,7 +179,7 @@ function NodeContextMenu({
|
|||
text='Конституенты'
|
||||
titleHtml='Перенос конституент</br>между схемами'
|
||||
icon={<IconChild size='1rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={isProcessing}
|
||||
onClick={handleRelocateConstituents}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -184,7 +187,7 @@ function NodeContextMenu({
|
|||
<DropdownButton
|
||||
text='Удалить операцию'
|
||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||
disabled={!controller.isMutable || controller.isProcessing || !controller.canDelete(operation.id)}
|
||||
disabled={!controller.isMutable || isProcessing || !controller.canDelete(operation.id)}
|
||||
onClick={handleDeleteOperation}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
|
|
@ -16,15 +16,23 @@ import {
|
|||
useReactFlow
|
||||
} from 'reactflow';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useLibrary } from '@/backend/library/useLibrary';
|
||||
import { useInputCreate } from '@/backend/oss/useInputCreate';
|
||||
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||
import { useOperationExecute } from '@/backend/oss/useOperationExecute';
|
||||
import { useUpdatePositions } from '@/backend/oss/useUpdatePositions';
|
||||
import { CProps } from '@/components/props';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { OssNode } from '@/models/miscellaneous';
|
||||
import { OperationID } from '@/models/oss';
|
||||
import { useMainHeight } from '@/stores/appLayout';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { useOSSGraphStore } from '@/stores/ossGraph';
|
||||
import { APP_COLORS } from '@/styling/color';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errors } from '@/utils/labels';
|
||||
import { errors, information } from '@/utils/labels';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
import { OssNodeTypes } from './graph/OssNodeTypes';
|
||||
|
@ -34,20 +42,24 @@ import ToolbarOssGraph from './ToolbarOssGraph';
|
|||
const ZOOM_MAX = 2;
|
||||
const ZOOM_MIN = 0.5;
|
||||
|
||||
interface OssFlowProps {
|
||||
isModified: boolean;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||
function OssFlow() {
|
||||
const mainHeight = useMainHeight();
|
||||
const controller = useOssEdit();
|
||||
const router = useConceptNavigation();
|
||||
const { items: libraryItems } = useLibrary();
|
||||
const flow = useReactFlow();
|
||||
const { setIsModified } = useModificationStore();
|
||||
|
||||
const isProcessing = useIsProcessingOss();
|
||||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||
|
||||
const { inputCreate } = useInputCreate();
|
||||
const { operationExecute } = useOperationExecute();
|
||||
const { updatePositions } = useUpdatePositions();
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [toggleReset, setToggleReset] = useState(false);
|
||||
|
@ -89,8 +101,8 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
type: edgeStraight ? 'straight' : 'simplebezier',
|
||||
animated: edgeAnimate,
|
||||
targetHandle:
|
||||
controller.schema!.operationByID.get(argument.argument)!.position_x >
|
||||
controller.schema!.operationByID.get(argument.operation)!.position_x
|
||||
controller.schema.operationByID.get(argument.argument)!.position_x >
|
||||
controller.schema.operationByID.get(argument.operation)!.position_x
|
||||
? 'right'
|
||||
: 'left'
|
||||
}))
|
||||
|
@ -117,7 +129,18 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
}
|
||||
|
||||
function handleSavePositions() {
|
||||
controller.savePositions(getPositions(), () => setIsModified(false));
|
||||
const positions = getPositions();
|
||||
updatePositions({ itemID: controller.schema.id, positions: positions }, () => {
|
||||
positions.forEach(item => {
|
||||
const operation = controller.schema.operationByID.get(item.id);
|
||||
if (operation) {
|
||||
operation.position_x = item.position_x;
|
||||
operation.position_y = item.position_y;
|
||||
}
|
||||
});
|
||||
toast.success(information.changesSaved);
|
||||
setIsModified(false);
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateOperation(inputs: OperationID[]) {
|
||||
|
@ -149,8 +172,19 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
handleDeleteOperation(controller.selected[0]);
|
||||
}
|
||||
|
||||
function handleCreateInput(target: OperationID) {
|
||||
controller.createInput(target, getPositions());
|
||||
function handleInputCreate(target: OperationID) {
|
||||
const operation = controller.schema.operationByID.get(target);
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
if (libraryItems.find(item => item.alias === operation.alias && item.location === controller.schema.location)) {
|
||||
toast.error(errors.inputAlreadyExists);
|
||||
return;
|
||||
}
|
||||
inputCreate({ itemID: controller.schema.id, data: { target: target, positions: getPositions() } }, new_schema => {
|
||||
toast.success(information.newLibraryItem);
|
||||
router.push(urls.schema(new_schema.id));
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditSchema(target: OperationID) {
|
||||
|
@ -161,15 +195,21 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
controller.promptEditOperation(target, getPositions());
|
||||
}
|
||||
|
||||
function handleExecuteOperation(target: OperationID) {
|
||||
controller.executeOperation(target, getPositions());
|
||||
function handleOperationExecute(target: OperationID) {
|
||||
operationExecute(
|
||||
{
|
||||
itemID: controller.schema.id, //
|
||||
data: { target: target, positions: getPositions() }
|
||||
},
|
||||
() => toast.success(information.operationExecuted)
|
||||
);
|
||||
}
|
||||
|
||||
function handleExecuteSelected() {
|
||||
if (controller.selected.length !== 1) {
|
||||
return;
|
||||
}
|
||||
handleExecuteOperation(controller.selected[0]);
|
||||
handleOperationExecute(controller.selected[0]);
|
||||
}
|
||||
|
||||
function handleRelocateConstituents(target: OperationID) {
|
||||
|
@ -237,14 +277,14 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (node.data.operation.result) {
|
||||
controller.openOperationSchema(Number(node.id));
|
||||
controller.navigateOperationSchema(Number(node.id));
|
||||
} else {
|
||||
handleEditOperation(Number(node.id));
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (controller.isProcessing) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (!controller.isMutable) {
|
||||
|
@ -274,7 +314,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
<div tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
<Overlay position='top-[1.9rem] pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
|
||||
<ToolbarOssGraph
|
||||
isModified={isModified}
|
||||
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
|
||||
onCreate={() => handleCreateOperation(controller.selected)}
|
||||
onDelete={handleDeleteSelected}
|
||||
|
@ -289,10 +328,10 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
<NodeContextMenu
|
||||
onHide={handleContextMenuHide}
|
||||
onDelete={handleDeleteOperation}
|
||||
onCreateInput={handleCreateInput}
|
||||
onCreateInput={handleInputCreate}
|
||||
onEditSchema={handleEditSchema}
|
||||
onEditOperation={handleEditOperation}
|
||||
onExecuteOperation={handleExecuteOperation}
|
||||
onExecuteOperation={handleOperationExecute}
|
||||
onRelocateConstituents={handleRelocateConstituents}
|
||||
{...menuProps}
|
||||
/>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||
import {
|
||||
IconAnimation,
|
||||
IconAnimationOff,
|
||||
|
@ -21,6 +22,7 @@ import BadgeHelp from '@/components/info/BadgeHelp';
|
|||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { OperationType } from '@/models/oss';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { useOSSGraphStore } from '@/stores/ossGraph';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip } from '@/utils/labels';
|
||||
|
@ -28,7 +30,6 @@ import { prepareTooltip } from '@/utils/labels';
|
|||
import { useOssEdit } from '../OssEditContext';
|
||||
|
||||
interface ToolbarOssGraphProps {
|
||||
isModified: boolean;
|
||||
onCreate: () => void;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
|
@ -40,7 +41,6 @@ interface ToolbarOssGraphProps {
|
|||
}
|
||||
|
||||
function ToolbarOssGraph({
|
||||
isModified,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onEdit,
|
||||
|
@ -51,6 +51,8 @@ function ToolbarOssGraph({
|
|||
onResetPositions
|
||||
}: ToolbarOssGraphProps) {
|
||||
const controller = useOssEdit();
|
||||
const { isModified } = useModificationStore();
|
||||
const isProcessing = useIsProcessingOss();
|
||||
const selectedOperation = controller.schema?.operationByID.get(controller.selected[0]);
|
||||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
|
@ -73,7 +75,7 @@ function ToolbarOssGraph({
|
|||
return false;
|
||||
}
|
||||
|
||||
const argumentOperations = argumentIDs.map(id => controller.schema!.operationByID.get(id)!);
|
||||
const argumentOperations = argumentIDs.map(id => controller.schema.operationByID.get(id)!);
|
||||
if (argumentOperations.some(item => item.result === null)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -144,35 +146,31 @@ function ToolbarOssGraph({
|
|||
<MiniButton
|
||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.isProcessing || !isModified}
|
||||
disabled={isProcessing || !isModified}
|
||||
onClick={onSavePositions}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={isProcessing}
|
||||
onClick={onCreate}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Активировать операцию'
|
||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
|
||||
disabled={isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
|
||||
onClick={onExecute}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.selected.length !== 1 || controller.isProcessing}
|
||||
disabled={controller.selected.length !== 1 || isProcessing}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
disabled={
|
||||
controller.selected.length !== 1 ||
|
||||
controller.isProcessing ||
|
||||
!controller.canDelete(controller.selected[0])
|
||||
}
|
||||
disabled={controller.selected.length !== 1 || isProcessing || !controller.canDelete(controller.selected[0])}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||
import {
|
||||
IconAdmin,
|
||||
IconAlert,
|
||||
|
@ -25,18 +26,17 @@ import useDropdown from '@/hooks/useDropdown';
|
|||
import { UserRole } from '@/models/user';
|
||||
import { useRoleStore } from '@/stores/role';
|
||||
import { describeAccessMode as describeUserRole, labelAccessMode as labelUserRole } from '@/utils/labels';
|
||||
import { sharePage } from '@/utils/utils';
|
||||
|
||||
import { useOssEdit } from './OssEditContext';
|
||||
|
||||
interface MenuOssTabsProps {
|
||||
onDestroy: () => void;
|
||||
}
|
||||
|
||||
function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
|
||||
function MenuOssTabs() {
|
||||
const controller = useOssEdit();
|
||||
const router = useConceptNavigation();
|
||||
const { user } = useAuth();
|
||||
|
||||
const isProcessing = useIsProcessingOss();
|
||||
|
||||
const role = useRoleStore(state => state.role);
|
||||
const setRole = useRoleStore(state => state.setRole);
|
||||
|
||||
|
@ -46,12 +46,12 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
|
|||
|
||||
function handleDelete() {
|
||||
schemaMenu.hide();
|
||||
onDestroy();
|
||||
controller.deleteSchema();
|
||||
}
|
||||
|
||||
function handleShare() {
|
||||
schemaMenu.hide();
|
||||
controller.share();
|
||||
sharePage();
|
||||
}
|
||||
|
||||
function handleChangeRole(newMode: UserRole) {
|
||||
|
@ -96,7 +96,7 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
|
|||
<DropdownButton
|
||||
text='Удалить схему'
|
||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||
disabled={controller.isProcessing || role < UserRole.OWNER}
|
||||
disabled={isProcessing || role < UserRole.OWNER}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -136,7 +136,7 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
|
|||
text='Конституенты'
|
||||
titleHtml='Перенос конституент</br>между схемами'
|
||||
icon={<IconChild size='1rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={isProcessing}
|
||||
onClick={handleRelocate}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
|
|
@ -6,24 +6,30 @@ import { toast } from 'react-toastify';
|
|||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { useLibrary } from '@/backend/library/useLibrary';
|
||||
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
|
||||
import { useSetEditors } from '@/backend/library/useSetEditors';
|
||||
import { useSetLocation } from '@/backend/library/useSetLocation';
|
||||
import { useSetOwner } from '@/backend/library/useSetOwner';
|
||||
import { useDeleteItem } from '@/backend/library/useDeleteItem';
|
||||
import { useInputUpdate } from '@/backend/oss/useInputUpdate';
|
||||
import { useOperationCreate } from '@/backend/oss/useOperationCreate';
|
||||
import { useOperationDelete } from '@/backend/oss/useOperationDelete';
|
||||
import { useOperationUpdate } from '@/backend/oss/useOperationUpdate';
|
||||
import { useOssSuspense } from '@/backend/oss/useOSS';
|
||||
import { AccessPolicy, ILibraryItemEditor, LibraryItemID } from '@/models/library';
|
||||
import { Position2D } from '@/models/miscellaneous';
|
||||
import { useRelocateConstituents } from '@/backend/oss/useRelocateConstituents';
|
||||
import { useUpdatePositions } from '@/backend/oss/useUpdatePositions';
|
||||
import { ILibraryItemEditor, LibraryItemID } from '@/models/library';
|
||||
import { calculateInsertPosition } from '@/models/miscellaneousAPI';
|
||||
import { IOperationPosition, IOperationSchema, OperationID, OperationType } from '@/models/oss';
|
||||
import { UserID, UserRole } from '@/models/user';
|
||||
import { UserRole } from '@/models/user';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { useRoleStore } from '@/stores/role';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errors, information } from '@/utils/labels';
|
||||
import { information, prompts } from '@/utils/labels';
|
||||
|
||||
import { RSTabID } from '../RSFormPage/RSTabs';
|
||||
import { RSTabID } from '../RSFormPage/RSEditContext';
|
||||
|
||||
export enum OssTabID {
|
||||
CARD = 0,
|
||||
GRAPH = 1
|
||||
}
|
||||
|
||||
export interface ICreateOperationPrompt {
|
||||
defaultX: number;
|
||||
|
@ -34,36 +40,27 @@ export interface ICreateOperationPrompt {
|
|||
}
|
||||
|
||||
export interface IOssEditContext extends ILibraryItemEditor {
|
||||
schema?: IOperationSchema;
|
||||
schema: IOperationSchema;
|
||||
selected: OperationID[];
|
||||
|
||||
isOwned: boolean;
|
||||
isMutable: boolean;
|
||||
isProcessing: boolean;
|
||||
isAttachedToOSS: boolean;
|
||||
|
||||
showTooltip: boolean;
|
||||
setShowTooltip: (newValue: boolean) => void;
|
||||
|
||||
setOwner: (newOwner: UserID) => void;
|
||||
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
||||
promptEditors: () => void;
|
||||
promptLocation: () => void;
|
||||
navigateTab: (tab: OssTabID) => void;
|
||||
navigateOperationSchema: (target: OperationID) => void;
|
||||
|
||||
deleteSchema: () => void;
|
||||
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||
|
||||
share: () => void;
|
||||
|
||||
openOperationSchema: (target: OperationID) => void;
|
||||
|
||||
savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
|
||||
promptCreateOperation: (props: ICreateOperationPrompt) => void;
|
||||
canDelete: (target: OperationID) => boolean;
|
||||
promptCreateOperation: (props: ICreateOperationPrompt) => void;
|
||||
promptDeleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
createInput: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
promptRelocateConstituents: (target: OperationID | undefined, positions: IOperationPosition[]) => void;
|
||||
}
|
||||
|
||||
|
@ -78,325 +75,229 @@ export const useOssEdit = () => {
|
|||
|
||||
interface OssEditStateProps {
|
||||
itemID: LibraryItemID;
|
||||
selected: OperationID[];
|
||||
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||
}
|
||||
|
||||
export const OssEditState = ({
|
||||
itemID,
|
||||
selected,
|
||||
setSelected,
|
||||
children
|
||||
}: React.PropsWithChildren<OssEditStateProps>) => {
|
||||
export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEditStateProps>) => {
|
||||
const router = useConceptNavigation();
|
||||
const { user } = useAuth();
|
||||
const { items: libraryItems } = useLibrary();
|
||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||
|
||||
const role = useRoleStore(state => state.role);
|
||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||
const model = useOssSuspense({ itemID: itemID });
|
||||
const { schema } = useOssSuspense({ itemID: itemID });
|
||||
|
||||
const { setOwner: setItemOwner } = useSetOwner();
|
||||
const { setLocation: setItemLocation } = useSetLocation();
|
||||
const { setAccessPolicy: setItemAccessPolicy } = useSetAccessPolicy();
|
||||
const { setEditors: setItemEditors } = useSetEditors();
|
||||
const isOwned = user?.id === schema.owner || false;
|
||||
|
||||
const isOwned = user?.id === model.schema?.owner || false;
|
||||
|
||||
const isMutable = role > UserRole.READER && !model.schema?.read_only;
|
||||
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(true);
|
||||
|
||||
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
|
||||
const [createCallback, setCreateCallback] = useState<((newID: OperationID) => void) | undefined>(undefined);
|
||||
const [selected, setSelected] = useState<OperationID[]>([]);
|
||||
|
||||
const showEditEditors = useDialogsStore(state => state.showEditEditors);
|
||||
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
|
||||
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
|
||||
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
|
||||
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
||||
|
||||
const [positions, setPositions] = useState<IOperationPosition[]>([]);
|
||||
const { deleteItem } = useDeleteItem();
|
||||
const { updatePositions } = useUpdatePositions();
|
||||
const { operationCreate } = useOperationCreate();
|
||||
const { operationDelete } = useOperationDelete();
|
||||
const { operationUpdate } = useOperationUpdate();
|
||||
const { relocateConstituents } = useRelocateConstituents();
|
||||
const { inputUpdate } = useInputUpdate();
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
adjustRole({
|
||||
isOwner: model.isOwned,
|
||||
isEditor: (user && model.schema?.editors.includes(user?.id)) ?? false,
|
||||
isOwner: isOwned,
|
||||
isEditor: (user && schema.editors.includes(user?.id)) ?? false,
|
||||
isStaff: user?.is_staff ?? false,
|
||||
adminMode: adminMode
|
||||
}),
|
||||
[model.schema, adjustRole, model.isOwned, user, adminMode]
|
||||
[schema, adjustRole, isOwned, user, adminMode]
|
||||
);
|
||||
|
||||
const handleSetLocation = (newLocation: string) =>
|
||||
setItemLocation({ itemID: model.itemID!, location: newLocation }, () => toast.success(information.moveComplete));
|
||||
function navigateTab(tab: OssTabID) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
const url = urls.oss_props({
|
||||
id: schema.id,
|
||||
tab: tab
|
||||
});
|
||||
router.push(url);
|
||||
}
|
||||
|
||||
const share = useCallback(() => {
|
||||
const currentRef = window.location.href;
|
||||
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => toast.success(information.linkReady))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const setOwner = (newOwner: UserID) =>
|
||||
setItemOwner({ itemID: model.itemID!, owner: newOwner }, () => toast.success(information.changesSaved));
|
||||
|
||||
const setAccessPolicy = (newPolicy: AccessPolicy) =>
|
||||
setItemAccessPolicy({ itemID: model.itemID!, policy: newPolicy }, () => toast.success(information.changesSaved));
|
||||
|
||||
const handleSetEditors = (newEditors: UserID[]) =>
|
||||
setItemEditors({ itemID: model.itemID!, editors: newEditors }, () => toast.success(information.changesSaved));
|
||||
|
||||
const openOperationSchema = useCallback(
|
||||
const navigateOperationSchema = useCallback(
|
||||
(target: OperationID) => {
|
||||
const node = model.schema?.operationByID.get(target);
|
||||
const node = schema.operationByID.get(target);
|
||||
if (!node?.result) {
|
||||
return;
|
||||
}
|
||||
router.push(urls.schema_props({ id: node.result, tab: RSTabID.CST_LIST }));
|
||||
},
|
||||
[router, model]
|
||||
[router, schema]
|
||||
);
|
||||
|
||||
const savePositions = useCallback(
|
||||
(positions: IOperationPosition[], callback?: () => void) => {
|
||||
model.savePositions({ positions: positions }, () => {
|
||||
positions.forEach(item => {
|
||||
const operation = model.schema?.operationByID.get(item.id);
|
||||
if (operation) {
|
||||
operation.position_x = item.position_x;
|
||||
operation.position_y = item.position_y;
|
||||
function deleteSchema() {
|
||||
if (!schema || !window.confirm(prompts.deleteOSS)) {
|
||||
return;
|
||||
}
|
||||
deleteItem(schema.id, () => {
|
||||
toast.success(information.itemDestroyed);
|
||||
router.push(urls.library);
|
||||
});
|
||||
toast.success(information.changesSaved);
|
||||
callback?.();
|
||||
});
|
||||
},
|
||||
[model]
|
||||
);
|
||||
}
|
||||
|
||||
const handleCreateOperation = useCallback(
|
||||
(data: IOperationCreateData) => {
|
||||
const target = calculateInsertPosition(
|
||||
model.schema!,
|
||||
data.item_data.operation_type,
|
||||
data.arguments,
|
||||
positions,
|
||||
insertPosition
|
||||
);
|
||||
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;
|
||||
model.createOperation(data, operation => {
|
||||
operationCreate({ itemID: schema.id, data }, operation => {
|
||||
toast.success(information.newOperation(operation.alias));
|
||||
if (createCallback) {
|
||||
setTimeout(() => createCallback(operation.id), PARAMETER.refreshTimeout);
|
||||
if (callback) {
|
||||
setTimeout(() => callback(operation.id), PARAMETER.refreshTimeout);
|
||||
}
|
||||
});
|
||||
},
|
||||
[model, positions, insertPosition, createCallback]
|
||||
);
|
||||
|
||||
const handleEditOperation = useCallback(
|
||||
(data: IOperationUpdateData) => {
|
||||
data.positions = positions;
|
||||
model.updateOperation(data, () => toast.success(information.changesSaved));
|
||||
},
|
||||
[model, positions]
|
||||
);
|
||||
|
||||
const canDelete = useCallback(
|
||||
(target: OperationID) => {
|
||||
if (!model.schema) {
|
||||
return false;
|
||||
initialInputs: inputs
|
||||
});
|
||||
}
|
||||
const operation = model.schema.operationByID.get(target);
|
||||
|
||||
function canDelete(target: OperationID) {
|
||||
const operation = 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]
|
||||
);
|
||||
return schema.graph.expandOutputs([target]).length === 0;
|
||||
}
|
||||
|
||||
const handleDeleteOperation = useCallback(
|
||||
(targetID: OperationID, keepConstituents: boolean, deleteSchema: boolean) => {
|
||||
const data: IOperationDeleteData = {
|
||||
function promptEditOperation(target: OperationID, positions: IOperationPosition[]) {
|
||||
const operation = schema.operationByID.get(target);
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
showEditOperation({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
onSubmit: data => {
|
||||
data.positions = positions;
|
||||
operationUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function promptDeleteOperation(target: OperationID, positions: IOperationPosition[]) {
|
||||
const operation = schema.operationByID.get(target);
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
showDeleteOperation({
|
||||
target: operation,
|
||||
onSubmit: (targetID, keepConstituents, deleteSchema) => {
|
||||
operationDelete(
|
||||
{
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
target: targetID,
|
||||
positions: positions,
|
||||
keep_constituents: keepConstituents,
|
||||
delete_schema: deleteSchema
|
||||
};
|
||||
model.deleteOperation(data, () => toast.success(information.operationDestroyed));
|
||||
}
|
||||
},
|
||||
[model, positions]
|
||||
() => toast.success(information.operationDestroyed)
|
||||
);
|
||||
|
||||
const createInput = useCallback(
|
||||
(target: OperationID, positions: IOperationPosition[]) => {
|
||||
const operation = model.schema?.operationByID.get(target);
|
||||
if (!model.schema || !operation) {
|
||||
return;
|
||||
}
|
||||
if (libraryItems.find(item => item.alias === operation.alias && item.location === model.schema!.location)) {
|
||||
toast.error(errors.inputAlreadyExists);
|
||||
return;
|
||||
}
|
||||
model.createInput({ target: target, positions: positions }, new_schema => {
|
||||
toast.success(information.newLibraryItem);
|
||||
router.push(urls.schema(new_schema.id));
|
||||
});
|
||||
},
|
||||
[model, libraryItems, router]
|
||||
);
|
||||
}
|
||||
|
||||
const setTargetInput = useCallback(
|
||||
(target: OperationID, newInput: LibraryItemID | undefined) => {
|
||||
const data: IOperationSetInputData = {
|
||||
function promptEditInput(target: OperationID, positions: IOperationPosition[]) {
|
||||
const operation = schema.operationByID.get(target);
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
showEditInput({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
onSubmit: (target, newInput) => {
|
||||
inputUpdate(
|
||||
{
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
target: target,
|
||||
positions: positions,
|
||||
input: newInput ?? null
|
||||
};
|
||||
model.setInput(data, () => toast.success(information.changesSaved));
|
||||
}
|
||||
},
|
||||
[model, positions]
|
||||
() => toast.success(information.changesSaved)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handleRelocateConstituents = useCallback(
|
||||
(data: ICstRelocateData) => {
|
||||
function promptRelocateConstituents(target: OperationID | undefined, positions: IOperationPosition[]) {
|
||||
const operation = target ? schema.operationByID.get(target) : undefined;
|
||||
showRelocateConstituents({
|
||||
oss: schema,
|
||||
initialTarget: operation,
|
||||
onSubmit: data => {
|
||||
if (
|
||||
positions.every(item => {
|
||||
const operation = model.schema!.operationByID.get(item.id)!;
|
||||
const operation = 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));
|
||||
relocateConstituents({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
|
||||
} else {
|
||||
model.savePositions({ positions: positions }, () =>
|
||||
model.relocateConstituents(data, () => toast.success(information.changesSaved))
|
||||
);
|
||||
}
|
||||
},
|
||||
[model, positions]
|
||||
);
|
||||
|
||||
const executeOperation = useCallback(
|
||||
(target: OperationID, positions: IOperationPosition[]) => {
|
||||
const data = {
|
||||
target: target,
|
||||
updatePositions(
|
||||
{
|
||||
itemID: schema.id, //
|
||||
positions: positions
|
||||
};
|
||||
model.executeOperation(data, () => toast.success(information.operationExecuted));
|
||||
},
|
||||
[model]
|
||||
() => relocateConstituents({ itemID: schema.id, data }, () => toast.success(information.changesSaved))
|
||||
);
|
||||
|
||||
const promptEditors = () => showEditEditors({ editors: model.schema?.editors ?? [], setEditors: handleSetEditors });
|
||||
|
||||
const promptLocation = () =>
|
||||
showEditLocation({ initial: model.schema?.location ?? '', onChangeLocation: handleSetLocation });
|
||||
|
||||
const promptCreateOperation = useCallback(
|
||||
({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) => {
|
||||
if (!model.schema) {
|
||||
return;
|
||||
}
|
||||
setInsertPosition({ x: defaultX, y: defaultY });
|
||||
setPositions(positions);
|
||||
setCreateCallback(() => callback);
|
||||
showCreateOperation({ oss: model.schema, onCreate: handleCreateOperation, initialInputs: inputs });
|
||||
},
|
||||
[model.schema, showCreateOperation, handleCreateOperation]
|
||||
);
|
||||
|
||||
const promptEditOperation = useCallback(
|
||||
(target: OperationID, positions: IOperationPosition[]) => {
|
||||
const operation = model.schema?.operationByID.get(target);
|
||||
if (!model.schema || !operation) {
|
||||
return;
|
||||
}
|
||||
setPositions(positions);
|
||||
showEditOperation({ oss: model.schema, target: operation, onSubmit: handleEditOperation });
|
||||
},
|
||||
[model.schema, showEditOperation, handleEditOperation]
|
||||
);
|
||||
|
||||
const promptDeleteOperation = useCallback(
|
||||
(target: OperationID, positions: IOperationPosition[]) => {
|
||||
const operation = model.schema?.operationByID.get(target);
|
||||
if (!model.schema || !operation) {
|
||||
return;
|
||||
});
|
||||
}
|
||||
setPositions(positions);
|
||||
showDeleteOperation({ target: operation, onSubmit: handleDeleteOperation });
|
||||
},
|
||||
[model.schema, showDeleteOperation, handleDeleteOperation]
|
||||
);
|
||||
|
||||
const promptEditInput = useCallback(
|
||||
(target: OperationID, positions: IOperationPosition[]) => {
|
||||
const operation = model.schema?.operationByID.get(target);
|
||||
if (!model.schema || !operation) {
|
||||
return;
|
||||
}
|
||||
setPositions(positions);
|
||||
showEditInput({ oss: model.schema, target: operation, onSubmit: setTargetInput });
|
||||
},
|
||||
[model.schema, showEditInput, setTargetInput]
|
||||
);
|
||||
|
||||
const promptRelocateConstituents = useCallback(
|
||||
(target: OperationID | undefined, positions: IOperationPosition[]) => {
|
||||
if (!model.schema) {
|
||||
return;
|
||||
}
|
||||
const operation = target ? model.schema?.operationByID.get(target) : undefined;
|
||||
setPositions(positions);
|
||||
showRelocateConstituents({ oss: model.schema, initialTarget: operation, onSubmit: handleRelocateConstituents });
|
||||
},
|
||||
[model.schema, showRelocateConstituents, handleRelocateConstituents]
|
||||
);
|
||||
|
||||
return (
|
||||
<OssEditContext
|
||||
value={{
|
||||
schema: model.schema,
|
||||
schema,
|
||||
selected,
|
||||
|
||||
navigateTab,
|
||||
|
||||
deleteSchema,
|
||||
|
||||
showTooltip,
|
||||
setShowTooltip,
|
||||
|
||||
isOwned,
|
||||
isMutable,
|
||||
isProcessing: model.processing,
|
||||
isAttachedToOSS: false,
|
||||
|
||||
setOwner,
|
||||
setAccessPolicy,
|
||||
promptEditors,
|
||||
promptLocation,
|
||||
|
||||
share,
|
||||
setSelected,
|
||||
|
||||
openOperationSchema,
|
||||
savePositions,
|
||||
navigateOperationSchema,
|
||||
promptCreateOperation,
|
||||
canDelete,
|
||||
promptDeleteOperation,
|
||||
createInput,
|
||||
promptEditInput,
|
||||
promptEditOperation,
|
||||
executeOperation,
|
||||
promptRelocateConstituents
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -1,23 +1,69 @@
|
|||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import axios from 'axios';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import { OssState } from '@/pages/OssPage/OssContext';
|
||||
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
|
||||
import { OssEditState } from './OssEditContext';
|
||||
import OssTabs from './OssTabs';
|
||||
|
||||
function OssPage() {
|
||||
const router = useConceptNavigation();
|
||||
const params = useParams();
|
||||
const itemID = params.id ? Number(params.id) : undefined;
|
||||
|
||||
const { isModified } = useModificationStore();
|
||||
useBlockNavigation(isModified);
|
||||
|
||||
// useBlockNavigation(
|
||||
// isModified &&
|
||||
// schema !== undefined &&
|
||||
// !!user &&
|
||||
// (user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
|
||||
// );
|
||||
|
||||
if (!itemID) {
|
||||
router.replace(urls.page404);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Loader />}>
|
||||
<OssState itemID={itemID}>
|
||||
<ErrorBoundary FallbackComponent={ProcessError}>
|
||||
<OssEditState itemID={itemID}>
|
||||
<OssTabs />
|
||||
</OssState>
|
||||
</Suspense>
|
||||
</OssEditState>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default OssPage;
|
||||
|
||||
// ====== Internals =========
|
||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
if (error.response.status === 404) {
|
||||
return (
|
||||
<div className='flex flex-col items-center p-2 mx-auto'>
|
||||
<p>{`Операционная схема с указанным идентификатором отсутствует`}</p>
|
||||
<div className='flex justify-center'>
|
||||
<TextURL text='Библиотека' href='/library' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (error.response.status === 403) {
|
||||
return (
|
||||
<div className='flex flex-col items-center p-2 mx-auto'>
|
||||
<p>Владелец ограничил доступ к данной схеме</p>
|
||||
<TextURL text='Библиотека' href='/library' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
|
|
|
@ -1,79 +1,46 @@
|
|||
'use client';
|
||||
|
||||
import axios from 'axios';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { useDeleteItem } from '@/backend/library/useDeleteItem';
|
||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { OperationID } from '@/models/oss';
|
||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||
import { information, prompts } from '@/utils/labels';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
|
||||
import EditorRSForm from './EditorOssCard';
|
||||
import EditorTermGraph from './EditorOssGraph';
|
||||
import MenuOssTabs from './MenuOssTabs';
|
||||
import { OssEditState } from './OssEditContext';
|
||||
|
||||
export enum OssTabID {
|
||||
CARD = 0,
|
||||
GRAPH = 1
|
||||
}
|
||||
import { OssTabID, useOssEdit } from './OssEditContext';
|
||||
|
||||
function OssTabs() {
|
||||
const router = useConceptNavigation();
|
||||
const query = useQueryStrings();
|
||||
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
|
||||
const { user } = useAuth();
|
||||
const { deleteItem } = useDeleteItem();
|
||||
|
||||
const { schema, navigateTab } = useOssEdit();
|
||||
|
||||
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
||||
const { schema, loading, loadingError: errorLoading } = useOSSControl();
|
||||
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const [selected, setSelected] = useState<OperationID[]>([]);
|
||||
useBlockNavigation(
|
||||
isModified &&
|
||||
schema !== undefined &&
|
||||
!!user &&
|
||||
(user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
|
||||
);
|
||||
const { setIsModified } = useModificationStore();
|
||||
|
||||
useEffect(() => setIsModified(false), [setIsModified]);
|
||||
|
||||
useEffect(() => {
|
||||
if (schema) {
|
||||
const oldTitle = document.title;
|
||||
document.title = schema.title;
|
||||
return () => {
|
||||
document.title = oldTitle;
|
||||
};
|
||||
}
|
||||
}, [schema, schema?.title]);
|
||||
}, [schema.title]);
|
||||
|
||||
useEffect(() => {
|
||||
hideFooter(activeTab === OssTabID.GRAPH);
|
||||
}, [activeTab, hideFooter]);
|
||||
|
||||
function navigateTab(tab: OssTabID) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
const url = urls.oss_props({
|
||||
id: schema.id,
|
||||
tab: tab
|
||||
});
|
||||
router.push(url);
|
||||
}
|
||||
|
||||
function onSelectTab(index: number, last: number, event: Event) {
|
||||
if (last === index) {
|
||||
return;
|
||||
|
@ -93,21 +60,7 @@ function OssTabs() {
|
|||
navigateTab(index);
|
||||
}
|
||||
|
||||
function onDestroySchema() {
|
||||
if (!schema || !window.confirm(prompts.deleteOSS)) {
|
||||
return;
|
||||
}
|
||||
deleteItem(schema.id, () => {
|
||||
toast.success(information.itemDestroyed);
|
||||
router.push(urls.library);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<OssEditState selected={selected} setSelected={setSelected}>
|
||||
{loading ? <Loader /> : null}
|
||||
{errorLoading ? <ProcessError error={errorLoading} /> : null}
|
||||
{schema && !loading ? (
|
||||
<Tabs
|
||||
selectedIndex={activeTab}
|
||||
onSelect={onSelectTab}
|
||||
|
@ -117,7 +70,7 @@ function OssTabs() {
|
|||
>
|
||||
<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} />
|
||||
<MenuOssTabs />
|
||||
|
||||
<TabLabel label='Карточка' title={schema.title ?? ''} />
|
||||
<TabLabel label='Граф' />
|
||||
|
@ -126,45 +79,15 @@ function OssTabs() {
|
|||
|
||||
<div className='overflow-x-hidden'>
|
||||
<TabPanel>
|
||||
<EditorRSForm
|
||||
isModified={isModified} //
|
||||
setIsModified={setIsModified}
|
||||
onDestroy={onDestroySchema}
|
||||
/>
|
||||
<EditorRSForm />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<EditorTermGraph isModified={isModified} setIsModified={setIsModified} />
|
||||
<EditorTermGraph />
|
||||
</TabPanel>
|
||||
</div>
|
||||
</Tabs>
|
||||
) : null}
|
||||
</OssEditState>
|
||||
);
|
||||
}
|
||||
|
||||
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} />;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -2,12 +2,18 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
||||
import { useMainHeight } from '@/stores/appLayout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { globals } from '@/utils/constants';
|
||||
import { information } from '@/utils/labels';
|
||||
import { promptUnsaved } from '@/utils/utils';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import ViewConstituents from '../ViewConstituents';
|
||||
|
@ -17,23 +23,20 @@ import ToolbarConstituenta from './ToolbarConstituenta';
|
|||
// Threshold window width to switch layout.
|
||||
const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
|
||||
|
||||
interface EditorConstituentaProps {
|
||||
activeCst?: IConstituenta;
|
||||
isModified: boolean;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }: EditorConstituentaProps) {
|
||||
function EditorConstituenta() {
|
||||
const controller = useRSEdit();
|
||||
const windowSize = useWindowSize();
|
||||
const mainHeight = useMainHeight();
|
||||
|
||||
const showList = usePreferencesStore(state => state.showCstSideList);
|
||||
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
|
||||
const { cstUpdate } = useCstUpdate();
|
||||
const { isModified } = useModificationStore();
|
||||
|
||||
const [toggleReset, setToggleReset] = useState(false);
|
||||
|
||||
const disabled = !activeCst || !controller.isContentEditable || controller.isProcessing;
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
const disabled = !controller.activeCst || !controller.isContentEditable || isProcessing;
|
||||
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
|
||||
|
||||
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
|
@ -56,6 +59,29 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
|||
}
|
||||
}
|
||||
|
||||
function handleEditTermForms() {
|
||||
if (!controller.activeCst) {
|
||||
return;
|
||||
}
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
showEditTerm({
|
||||
target: controller.activeCst,
|
||||
onSave: forms =>
|
||||
cstUpdate(
|
||||
{
|
||||
itemID: controller.schema.id,
|
||||
data: {
|
||||
target: controller.activeCst!.id,
|
||||
item_data: { term_forms: forms }
|
||||
}
|
||||
},
|
||||
() => toast.success(information.changesSaved)
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
function initiateSubmit() {
|
||||
const element = document.getElementById(globals.constituenta_editor) as HTMLFormElement;
|
||||
if (element) {
|
||||
|
@ -76,9 +102,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
|||
return (
|
||||
<>
|
||||
<ToolbarConstituenta
|
||||
activeCst={activeCst}
|
||||
activeCst={controller.activeCst}
|
||||
disabled={disabled}
|
||||
modified={isModified}
|
||||
onSubmit={initiateSubmit}
|
||||
onReset={() => setToggleReset(prev => !prev)}
|
||||
/>
|
||||
|
@ -97,21 +122,13 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
|||
<FormConstituenta
|
||||
disabled={disabled}
|
||||
id={globals.constituenta_editor}
|
||||
state={activeCst}
|
||||
isModified={isModified}
|
||||
toggleReset={toggleReset}
|
||||
setIsModified={setIsModified}
|
||||
onEditTerm={controller.editTermForms}
|
||||
onRename={controller.renameCst}
|
||||
onOpenEdit={onOpenEdit}
|
||||
onEditTerm={handleEditTermForms}
|
||||
/>
|
||||
<ViewConstituents
|
||||
isMounted={showList}
|
||||
schema={controller.schema}
|
||||
expression={activeCst?.definition_formal ?? ''}
|
||||
expression={controller.activeCst?.definition_formal ?? ''}
|
||||
isBottom={isNarrow}
|
||||
activeCst={activeCst}
|
||||
onOpenEdit={onOpenEdit}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -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;
|
|
@ -6,6 +6,7 @@ import { toast } from 'react-toastify';
|
|||
|
||||
import { ICstUpdateDTO } from '@/backend/rsform/api';
|
||||
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
|
||||
import { CProps } from '@/components/props';
|
||||
import RefsInput from '@/components/RefsInput';
|
||||
|
@ -13,27 +14,22 @@ import Indicator from '@/components/ui/Indicator';
|
|||
import Overlay from '@/components/ui/Overlay';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
|
||||
import { ConstituentaID, CstType } from '@/models/rsform';
|
||||
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
|
||||
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { errors, information, labelCstTypification } from '@/utils/labels';
|
||||
|
||||
import EditorRSExpression from '../EditorRSExpression';
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import ControlsOverlay from './ControlsOverlay';
|
||||
import EditorControls from './EditorControls';
|
||||
|
||||
interface FormConstituentaProps {
|
||||
disabled: boolean;
|
||||
|
||||
id?: string;
|
||||
state?: IConstituenta;
|
||||
|
||||
isModified: boolean;
|
||||
disabled: boolean;
|
||||
toggleReset: boolean;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
|
||||
onRename: () => void;
|
||||
onEditTerm: () => void;
|
||||
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
@ -41,72 +37,68 @@ interface FormConstituentaProps {
|
|||
function FormConstituenta({
|
||||
disabled,
|
||||
id,
|
||||
state,
|
||||
|
||||
isModified,
|
||||
setIsModified,
|
||||
|
||||
toggleReset,
|
||||
onRename,
|
||||
onEditTerm,
|
||||
onOpenEdit
|
||||
}: FormConstituentaProps) {
|
||||
const { cstUpdate } = useCstUpdate();
|
||||
const controller = useRSEdit();
|
||||
const schema = controller.schema;
|
||||
const { schema, activeCst } = useRSEdit();
|
||||
const { isModified, setIsModified } = useModificationStore();
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
|
||||
const [term, setTerm] = useState(state?.term_raw ?? '');
|
||||
const [textDefinition, setTextDefinition] = useState(state?.definition_raw ?? '');
|
||||
const [expression, setExpression] = useState(state?.definition_formal ?? '');
|
||||
const [convention, setConvention] = useState(state?.convention ?? '');
|
||||
const [term, setTerm] = useState(activeCst?.term_raw ?? '');
|
||||
const [textDefinition, setTextDefinition] = useState(activeCst?.definition_raw ?? '');
|
||||
const [expression, setExpression] = useState(activeCst?.definition_formal ?? '');
|
||||
const [convention, setConvention] = useState(activeCst?.convention ?? '');
|
||||
const [typification, setTypification] = useState('N/A');
|
||||
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
|
||||
const typeInfo = state
|
||||
const typeInfo = activeCst
|
||||
? {
|
||||
alias: state.alias,
|
||||
result: localParse ? localParse.typification : state.parse.typification,
|
||||
args: localParse ? localParse.args : state.parse.args
|
||||
alias: activeCst.alias,
|
||||
result: localParse ? localParse.typification : activeCst.parse.typification,
|
||||
args: localParse ? localParse.args : activeCst.parse.args
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const [forceComment, setForceComment] = useState(false);
|
||||
|
||||
const isBasic = !!state && isBasicConcept(state.cst_type);
|
||||
const isElementary = !!state && isBaseSet(state.cst_type);
|
||||
const showConvention = !state || !!state.convention || forceComment || isBasic;
|
||||
const isBasic = !!activeCst && isBasicConcept(activeCst.cst_type);
|
||||
const isElementary = !!activeCst && isBaseSet(activeCst.cst_type);
|
||||
const showConvention = !activeCst || !!activeCst.convention || forceComment || isBasic;
|
||||
|
||||
const showTypification = useDialogsStore(state => state.showShowTypeGraph);
|
||||
const showTypification = useDialogsStore(activeCst => activeCst.showShowTypeGraph);
|
||||
|
||||
useEffect(() => {
|
||||
if (state) {
|
||||
setConvention(state.convention);
|
||||
setTerm(state.term_raw);
|
||||
setTextDefinition(state.definition_raw);
|
||||
setExpression(state.definition_formal);
|
||||
setTypification(state ? labelCstTypification(state) : 'N/A');
|
||||
if (activeCst) {
|
||||
setConvention(activeCst.convention);
|
||||
setTerm(activeCst.term_raw);
|
||||
setTextDefinition(activeCst.definition_raw);
|
||||
setExpression(activeCst.definition_formal);
|
||||
setTypification(activeCst ? labelCstTypification(activeCst) : 'N/A');
|
||||
setForceComment(false);
|
||||
setLocalParse(undefined);
|
||||
}
|
||||
}, [state, schema, toggleReset, setIsModified]);
|
||||
}, [activeCst, schema, toggleReset, setIsModified]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!state) {
|
||||
if (!activeCst) {
|
||||
setIsModified(false);
|
||||
return;
|
||||
}
|
||||
setIsModified(
|
||||
state.term_raw !== term ||
|
||||
state.definition_raw !== textDefinition ||
|
||||
state.convention !== convention ||
|
||||
state.definition_formal !== expression
|
||||
activeCst.term_raw !== term ||
|
||||
activeCst.definition_raw !== textDefinition ||
|
||||
activeCst.convention !== convention ||
|
||||
activeCst.definition_formal !== expression
|
||||
);
|
||||
return () => setIsModified(false);
|
||||
}, [
|
||||
state,
|
||||
state?.term_raw,
|
||||
state?.definition_formal,
|
||||
state?.definition_raw,
|
||||
state?.convention,
|
||||
activeCst,
|
||||
activeCst?.term_raw,
|
||||
activeCst?.definition_formal,
|
||||
activeCst?.definition_raw,
|
||||
activeCst?.convention,
|
||||
term,
|
||||
textDefinition,
|
||||
expression,
|
||||
|
@ -118,23 +110,23 @@ function FormConstituenta({
|
|||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (!state || controller.isProcessing || !schema) {
|
||||
if (!activeCst || isProcessing || !schema) {
|
||||
return;
|
||||
}
|
||||
const data: ICstUpdateDTO = {
|
||||
target: state.id,
|
||||
target: activeCst.id,
|
||||
item_data: {
|
||||
term_raw: state.term_raw !== term ? term : undefined,
|
||||
definition_formal: state.definition_formal !== expression ? expression : undefined,
|
||||
definition_raw: state.definition_raw !== textDefinition ? textDefinition : undefined,
|
||||
convention: state.convention !== convention ? convention : undefined
|
||||
term_raw: activeCst.term_raw !== term ? term : undefined,
|
||||
definition_formal: activeCst.definition_formal !== expression ? expression : undefined,
|
||||
definition_raw: activeCst.definition_raw !== textDefinition ? textDefinition : undefined,
|
||||
convention: activeCst.convention !== convention ? convention : undefined
|
||||
}
|
||||
};
|
||||
cstUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
|
||||
}
|
||||
|
||||
function handleTypeGraph(event: CProps.EventMouse) {
|
||||
if (!state || (localParse && !localParse.parseResult) || state.parse.status !== ParsingStatus.VERIFIED) {
|
||||
if (!activeCst || (localParse && !localParse.parseResult) || activeCst.parse.status !== ParsingStatus.VERIFIED) {
|
||||
toast.error(errors.typeStructureFailed);
|
||||
return;
|
||||
}
|
||||
|
@ -145,16 +137,7 @@ function FormConstituenta({
|
|||
|
||||
return (
|
||||
<div className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'>
|
||||
{state ? (
|
||||
<ControlsOverlay
|
||||
disabled={disabled}
|
||||
modified={isModified}
|
||||
processing={controller.isProcessing}
|
||||
constituenta={state}
|
||||
onEditTerm={onEditTerm}
|
||||
onRename={onRename}
|
||||
/>
|
||||
) : null}
|
||||
{activeCst ? <EditorControls disabled={disabled} constituenta={activeCst} onEditTerm={onEditTerm} /> : null}
|
||||
<form id={id} className={clsx('cc-column', 'mt-1 md:w-[48.8rem] shrink-0', 'px-6 py-1')} onSubmit={handleSubmit}>
|
||||
<RefsInput
|
||||
key='cst_term'
|
||||
|
@ -165,12 +148,12 @@ function FormConstituenta({
|
|||
schema={schema}
|
||||
onOpenEdit={onOpenEdit}
|
||||
value={term}
|
||||
initialValue={state?.term_raw ?? ''}
|
||||
resolved={state?.term_resolved ?? 'Конституента не выбрана'}
|
||||
initialValue={activeCst?.term_raw ?? ''}
|
||||
resolved={activeCst?.term_resolved ?? 'Конституента не выбрана'}
|
||||
disabled={disabled}
|
||||
onChange={newValue => setTerm(newValue)}
|
||||
/>
|
||||
{state ? (
|
||||
{activeCst ? (
|
||||
<TextArea
|
||||
id='cst_typification'
|
||||
fitContent
|
||||
|
@ -184,24 +167,26 @@ function FormConstituenta({
|
|||
colors='bg-transparent clr-text-default cursor-default'
|
||||
/>
|
||||
) : null}
|
||||
{state ? (
|
||||
{activeCst ? (
|
||||
<>
|
||||
{!!state.definition_formal || !isElementary ? (
|
||||
{!!activeCst.definition_formal || !isElementary ? (
|
||||
<EditorRSExpression
|
||||
id='cst_expression'
|
||||
label={
|
||||
state.cst_type === CstType.STRUCTURED
|
||||
activeCst.cst_type === CstType.STRUCTURED
|
||||
? 'Область определения'
|
||||
: isFunctional(state.cst_type)
|
||||
: isFunctional(activeCst.cst_type)
|
||||
? 'Определение функции'
|
||||
: 'Формальное определение'
|
||||
}
|
||||
placeholder={
|
||||
state.cst_type !== CstType.STRUCTURED ? 'Родоструктурное выражение' : 'Типизация родовой структуры'
|
||||
activeCst.cst_type !== CstType.STRUCTURED
|
||||
? 'Родоструктурное выражение'
|
||||
: 'Типизация родовой структуры'
|
||||
}
|
||||
value={expression}
|
||||
activeCst={state}
|
||||
disabled={disabled || state.is_inherited}
|
||||
activeCst={activeCst}
|
||||
disabled={disabled || activeCst.is_inherited}
|
||||
toggleReset={toggleReset}
|
||||
onChangeExpression={newValue => setExpression(newValue)}
|
||||
onChangeTypification={setTypification}
|
||||
|
@ -210,7 +195,7 @@ function FormConstituenta({
|
|||
onShowTypeGraph={handleTypeGraph}
|
||||
/>
|
||||
) : null}
|
||||
{!!state.definition_raw || !isElementary ? (
|
||||
{!!activeCst.definition_raw || !isElementary ? (
|
||||
<RefsInput
|
||||
id='cst_definition'
|
||||
label='Текстовое определение'
|
||||
|
@ -220,8 +205,8 @@ function FormConstituenta({
|
|||
schema={schema}
|
||||
onOpenEdit={onOpenEdit}
|
||||
value={textDefinition}
|
||||
initialValue={state.definition_raw}
|
||||
resolved={state.definition_resolved}
|
||||
initialValue={activeCst.definition_raw}
|
||||
resolved={activeCst.definition_resolved}
|
||||
disabled={disabled}
|
||||
onChange={newValue => setTextDefinition(newValue)}
|
||||
/>
|
||||
|
@ -236,12 +221,12 @@ function FormConstituenta({
|
|||
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
||||
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
||||
value={convention}
|
||||
disabled={disabled || (isBasic && state.is_inherited)}
|
||||
disabled={disabled || (isBasic && activeCst.is_inherited)}
|
||||
onChange={event => setConvention(event.target.value)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!showConvention && (!disabled || controller.isProcessing) ? (
|
||||
{!showConvention && (!disabled || isProcessing) ? (
|
||||
<button
|
||||
key='cst_disable_comment'
|
||||
id='cst_disable_comment'
|
||||
|
@ -254,7 +239,7 @@ function FormConstituenta({
|
|||
</button>
|
||||
) : null}
|
||||
|
||||
{!disabled || controller.isProcessing ? (
|
||||
{!disabled || isProcessing ? (
|
||||
<div className='mx-auto flex'>
|
||||
<SubmitButton
|
||||
key='cst_form_submit'
|
||||
|
@ -264,13 +249,13 @@ function FormConstituenta({
|
|||
icon={<IconSave size='1.25rem' />}
|
||||
/>
|
||||
<Overlay position='top-[0.1rem] left-[0.4rem]' className='cc-icons'>
|
||||
{state.has_inherited_children && !state.is_inherited ? (
|
||||
{activeCst.has_inherited_children && !activeCst.is_inherited ? (
|
||||
<Indicator
|
||||
icon={<IconPredecessor size='1.25rem' className='text-sec-600' />}
|
||||
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза'
|
||||
/>
|
||||
) : null}
|
||||
{state.is_inherited ? (
|
||||
{activeCst.is_inherited ? (
|
||||
<Indicator
|
||||
icon={<IconChild size='1.25rem' className='text-sec-600' />}
|
||||
titleHtml='Внимание!</br> Конституента является наследником<br/>'
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useFindPredecessor } from '@/backend/oss/useFindPredecessor';
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import {
|
||||
IconClone,
|
||||
IconDestroy,
|
||||
|
@ -19,17 +23,17 @@ import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
|||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip, tooltips } from '@/utils/labels';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import { RSTabID, useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface ToolbarConstituentaProps {
|
||||
activeCst?: IConstituenta;
|
||||
disabled: boolean;
|
||||
modified: boolean;
|
||||
|
||||
onSubmit: () => void;
|
||||
onReset: () => void;
|
||||
|
@ -38,15 +42,30 @@ interface ToolbarConstituentaProps {
|
|||
function ToolbarConstituenta({
|
||||
activeCst,
|
||||
disabled,
|
||||
modified,
|
||||
|
||||
onSubmit,
|
||||
onReset
|
||||
}: ToolbarConstituentaProps) {
|
||||
const controller = useRSEdit();
|
||||
const router = useConceptNavigation();
|
||||
const { findPredecessor } = useFindPredecessor();
|
||||
|
||||
const showList = usePreferencesStore(state => state.showCstSideList);
|
||||
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
|
||||
const { isModified } = useModificationStore();
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
|
||||
function viewPredecessor(target: ConstituentaID) {
|
||||
findPredecessor({ target: target }, reference =>
|
||||
router.push(
|
||||
urls.schema_props({
|
||||
id: reference.schema,
|
||||
active: reference.id,
|
||||
tab: RSTabID.CST_EDIT
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
|
@ -56,13 +75,13 @@ function ToolbarConstituenta({
|
|||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||
<MiniSelectorOSS
|
||||
items={controller.schema.oss}
|
||||
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
|
||||
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
|
||||
/>
|
||||
) : null}
|
||||
{activeCst?.is_inherited ? (
|
||||
<MiniButton
|
||||
title='Перейти к исходной конституенте в ОСС'
|
||||
onClick={() => controller.viewPredecessor(activeCst.id)}
|
||||
onClick={() => viewPredecessor(activeCst.id)}
|
||||
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -71,25 +90,25 @@ function ToolbarConstituenta({
|
|||
<MiniButton
|
||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||
disabled={disabled || !modified}
|
||||
disabled={disabled || !isModified}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Сбросить несохраненные изменения'
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
disabled={disabled || !modified}
|
||||
disabled={disabled || !isModified}
|
||||
onClick={onReset}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Создать конституенту после данной'
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
||||
disabled={!controller.isContentEditable || isProcessing}
|
||||
onClick={() => controller.createCst(activeCst?.cst_type, false)}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={modified ? tooltips.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
||||
titleHtml={isModified ? tooltips.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
||||
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||
disabled={disabled || modified}
|
||||
disabled={disabled || isModified}
|
||||
onClick={controller.cloneCst}
|
||||
/>
|
||||
<MiniButton
|
||||
|
@ -112,13 +131,13 @@ function ToolbarConstituenta({
|
|||
<MiniButton
|
||||
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
||||
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
|
||||
disabled={disabled || modified || (controller.schema && controller.schema?.items.length < 2)}
|
||||
disabled={disabled || isModified || (controller.schema && controller.schema?.items.length < 2)}
|
||||
onClick={controller.moveUp}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
|
||||
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
|
||||
disabled={disabled || modified || (controller.schema && controller.schema?.items.length < 2)}
|
||||
disabled={disabled || isModified || (controller.schema && controller.schema?.items.length < 2)}
|
||||
onClick={controller.moveDown}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import { CProps } from '@/components/props';
|
||||
import RSInput from '@/components/RSInput';
|
||||
|
@ -62,6 +63,7 @@ function EditorRSExpression({
|
|||
const parser = useRSParse({ schema: controller.schema });
|
||||
const rsInput = useRef<ReactCodeMirrorRef>(null);
|
||||
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
const showControls = usePreferencesStore(state => state.showExpressionControls);
|
||||
const showAST = useDialogsStore(state => state.showShowAST);
|
||||
|
||||
|
@ -173,7 +175,7 @@ function EditorRSExpression({
|
|||
/>
|
||||
|
||||
<RSEditorControls
|
||||
isOpen={showControls && (!disabled || (controller.isProcessing && !activeCst.is_inherited))}
|
||||
isOpen={showControls && (!disabled || (isProcessing && !activeCst.is_inherited))}
|
||||
disabled={disabled}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { Suspense, useCallback } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
|
||||
import { useLibraryItem } from '@/backend/library/useLibraryItem';
|
||||
import { useSetEditors } from '@/backend/library/useSetEditors';
|
||||
import { useSetLocation } from '@/backend/library/useSetLocation';
|
||||
import { useSetOwner } from '@/backend/library/useSetOwner';
|
||||
import { useLabelUser } from '@/backend/users/useLabelUser';
|
||||
import {
|
||||
IconDateCreate,
|
||||
|
@ -21,29 +27,41 @@ import Overlay from '@/components/ui/Overlay';
|
|||
import Tooltip from '@/components/ui/Tooltip';
|
||||
import ValueIcon from '@/components/ui/ValueIcon';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
|
||||
import { ILibraryItemEditor, LibraryItemID, LibraryItemType } from '@/models/library';
|
||||
import { UserID, UserRole } from '@/models/user';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { useRoleStore } from '@/stores/role';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { prompts } from '@/utils/labels';
|
||||
import { information, prompts } from '@/utils/labels';
|
||||
|
||||
interface EditorLibraryItemProps {
|
||||
item?: ILibraryItemData;
|
||||
isModified?: boolean;
|
||||
itemID: LibraryItemID;
|
||||
itemType: LibraryItemType;
|
||||
controller: ILibraryItemEditor;
|
||||
}
|
||||
|
||||
function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemProps) {
|
||||
function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemProps) {
|
||||
const getUserLabel = useLabelUser();
|
||||
const role = useRoleStore(state => state.role);
|
||||
const intl = useIntl();
|
||||
const router = useConceptNavigation();
|
||||
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
||||
const setGlobalLocation = useLibrarySearchStore(state => state.setLocation);
|
||||
|
||||
const { item } = useLibraryItem({ itemID, itemType });
|
||||
const isProcessing = useIsProcessingLibrary();
|
||||
const { isModified } = useModificationStore();
|
||||
|
||||
const { setOwner } = useSetOwner();
|
||||
const { setLocation } = useSetLocation();
|
||||
const { setEditors } = useSetEditors();
|
||||
|
||||
const showEditEditors = useDialogsStore(state => state.showEditEditors);
|
||||
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
|
||||
|
||||
const ownerSelector = useDropdown();
|
||||
const onSelectUser = useCallback(
|
||||
(newValue: UserID) => {
|
||||
const onSelectUser = function (newValue: UserID) {
|
||||
ownerSelector.hide();
|
||||
if (newValue === item?.owner) {
|
||||
return;
|
||||
|
@ -51,21 +69,38 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
|||
if (!window.confirm(prompts.ownerChange)) {
|
||||
return;
|
||||
}
|
||||
controller.setOwner(newValue);
|
||||
},
|
||||
[controller, item?.owner, ownerSelector]
|
||||
);
|
||||
setOwner({ itemID: itemID, owner: newValue }, () => toast.success(information.changesSaved));
|
||||
};
|
||||
|
||||
const handleOpenLibrary = useCallback(
|
||||
(event: CProps.EventMouse) => {
|
||||
function handleOpenLibrary(event: CProps.EventMouse) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
setLocation(item.location);
|
||||
setGlobalLocation(item.location);
|
||||
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) {
|
||||
return null;
|
||||
|
@ -86,8 +121,8 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
|||
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
|
||||
value={item.location}
|
||||
title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'}
|
||||
onClick={controller.promptLocation}
|
||||
disabled={isModified || controller.isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
|
||||
onClick={handleEditLocation}
|
||||
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -108,7 +143,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
|||
value={getUserLabel(item.owner)}
|
||||
title={controller.isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'}
|
||||
onClick={ownerSelector.toggle}
|
||||
disabled={isModified || controller.isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
|
||||
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
|
||||
/>
|
||||
|
||||
<div className='sm:mb-1 flex justify-between items-center'>
|
||||
|
@ -117,8 +152,8 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
|||
dense
|
||||
icon={<IconEditor size='1.25rem' className='icon-primary' />}
|
||||
value={item.editors.length}
|
||||
onClick={controller.promptEditors}
|
||||
disabled={isModified || controller.isProcessing || role < UserRole.OWNER}
|
||||
onClick={handleEditEditors}
|
||||
disabled={isModified || isProcessing || role < UserRole.OWNER}
|
||||
/>
|
||||
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
|
||||
<Suspense fallback={<Loader scale={2} />}>
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import FlexColumn from '@/components/ui/FlexColumn';
|
||||
import { LibraryItemType } from '@/models/library';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
@ -11,14 +13,9 @@ import FormRSForm from './FormRSForm';
|
|||
import RSFormStats from './RSFormStats';
|
||||
import ToolbarRSFormCard from './ToolbarRSFormCard';
|
||||
|
||||
interface EditorRSFormCardProps {
|
||||
isModified: boolean;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
onDestroy: () => void;
|
||||
}
|
||||
|
||||
function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSFormCardProps) {
|
||||
function EditorRSFormCard() {
|
||||
const controller = useRSEdit();
|
||||
const { isModified } = useModificationStore();
|
||||
|
||||
function initiateSubmit() {
|
||||
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
|
||||
|
@ -38,12 +35,7 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
|
|||
|
||||
return (
|
||||
<>
|
||||
<ToolbarRSFormCard
|
||||
modified={isModified}
|
||||
onSubmit={initiateSubmit}
|
||||
onDestroy={onDestroy}
|
||||
controller={controller}
|
||||
/>
|
||||
<ToolbarRSFormCard onSubmit={initiateSubmit} controller={controller} />
|
||||
<div
|
||||
onKeyDown={handleInput}
|
||||
className={clsx(
|
||||
|
@ -53,8 +45,8 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
|
|||
)}
|
||||
>
|
||||
<FlexColumn className='flex-shrink'>
|
||||
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
||||
<EditorLibraryItem item={controller.schema} isModified={isModified} controller={controller} />
|
||||
<FormRSForm id={globals.library_item_editor} />
|
||||
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.RSFORM} controller={controller} />
|
||||
</FlexColumn>
|
||||
|
||||
{controller.schema ? <RSFormStats stats={controller.schema.stats} isArchive={controller.isArchive} /> : null}
|
||||
|
|
|
@ -3,15 +3,19 @@
|
|||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { ILibraryUpdateDTO } from '@/backend/library/api';
|
||||
import { useUpdateItem } from '@/backend/library/useUpdateItem';
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import { IconSave } from '@/components/Icons';
|
||||
import SelectVersion from '@/components/select/SelectVersion';
|
||||
import Label from '@/components/ui/Label';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { LibraryItemType } from '@/models/library';
|
||||
import { LibraryItemType, VersionID } from '@/models/library';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import ToolbarItemAccess from './ToolbarItemAccess';
|
||||
|
@ -19,14 +23,15 @@ import ToolbarVersioning from './ToolbarVersioning';
|
|||
|
||||
interface FormRSFormProps {
|
||||
id?: string;
|
||||
isModified: boolean;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||
function FormRSForm({ id }: FormRSFormProps) {
|
||||
const controller = useRSEdit();
|
||||
const router = useConceptNavigation();
|
||||
const schema = controller.schema;
|
||||
const { updateItem: update } = useUpdateItem();
|
||||
const { isModified, setIsModified } = useModificationStore();
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
|
||||
const [title, setTitle] = useState(schema?.title ?? '');
|
||||
const [alias, setAlias] = useState(schema?.alias ?? '');
|
||||
|
@ -34,6 +39,10 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
|||
const [visible, setVisible] = useState(schema?.visible ?? false);
|
||||
const [readOnly, setReadOnly] = useState(schema?.read_only ?? false);
|
||||
|
||||
function handleSelectVersion(version?: VersionID) {
|
||||
router.push(urls.schema(schema.id, version));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (schema) {
|
||||
setTitle(schema.title);
|
||||
|
@ -127,7 +136,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
|||
className='select-none'
|
||||
value={schema?.version} //
|
||||
items={schema?.versions}
|
||||
onSelectValue={controller.viewVersion}
|
||||
onSelectValue={handleSelectVersion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -137,14 +146,14 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
|||
label='Описание'
|
||||
rows={3}
|
||||
value={comment}
|
||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
||||
disabled={!controller.isContentEditable || isProcessing}
|
||||
onChange={event => setComment(event.target.value)}
|
||||
/>
|
||||
{controller.isContentEditable || isModified ? (
|
||||
<SubmitButton
|
||||
text='Сохранить изменения'
|
||||
className='self-center mt-4'
|
||||
loading={controller.isProcessing}
|
||||
loading={isProcessing}
|
||||
disabled={!isModified}
|
||||
icon={<IconSave size='1.25rem' />}
|
||||
/>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
|
||||
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
|
||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||
import { IconImmutable, IconMutable } from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
|
@ -10,6 +14,7 @@ import { HelpTopic } from '@/models/miscellaneous';
|
|||
import { UserRole } from '@/models/user';
|
||||
import { useRoleStore } from '@/stores/role';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { information } from '@/utils/labels';
|
||||
|
||||
interface ToolbarItemAccessProps {
|
||||
visible: boolean;
|
||||
|
@ -21,23 +26,29 @@ interface ToolbarItemAccessProps {
|
|||
|
||||
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) {
|
||||
const role = useRoleStore(state => state.role);
|
||||
const policy = controller.schema?.access_policy ?? AccessPolicy.PRIVATE;
|
||||
const isProcessing = useIsProcessingLibrary();
|
||||
const policy = controller.schema.access_policy;
|
||||
const { setAccessPolicy } = useSetAccessPolicy();
|
||||
|
||||
function handleSetAccessPolicy(newPolicy: AccessPolicy) {
|
||||
setAccessPolicy({ itemID: controller.schema.id, policy: newPolicy }, () => toast.success(information.changesSaved));
|
||||
}
|
||||
|
||||
return (
|
||||
<Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'>
|
||||
<Label text='Доступ' className='self-center select-none' />
|
||||
<div className='ml-auto cc-icons'>
|
||||
<SelectAccessPolicy
|
||||
disabled={role <= UserRole.EDITOR || controller.isProcessing || controller.isAttachedToOSS}
|
||||
disabled={role <= UserRole.EDITOR || isProcessing || controller.isAttachedToOSS}
|
||||
value={policy}
|
||||
onChange={newPolicy => controller.setAccessPolicy(newPolicy)}
|
||||
onChange={handleSetAccessPolicy}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||
icon={<VisibilityIcon value={visible} />}
|
||||
onClick={toggleVisible}
|
||||
disabled={role === UserRole.READER || controller.isProcessing}
|
||||
disabled={role === UserRole.READER || isProcessing}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
|
@ -50,7 +61,7 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
|
|||
)
|
||||
}
|
||||
onClick={toggleReadOnly}
|
||||
disabled={role === UserRole.READER || controller.isProcessing}
|
||||
disabled={role === UserRole.READER || isProcessing}
|
||||
/>
|
||||
|
||||
<BadgeHelp topic={HelpTopic.ACCESS} className={PARAMETER.TOOLTIP_WIDTH} offset={4} />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
|
||||
import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
||||
|
@ -9,22 +10,24 @@ import { AccessPolicy, ILibraryItemEditor, LibraryItemType } from '@/models/libr
|
|||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IRSForm } from '@/models/rsform';
|
||||
import { UserRole } from '@/models/user';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { useRoleStore } from '@/stores/role';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip, tooltips } from '@/utils/labels';
|
||||
import { sharePage } from '@/utils/utils';
|
||||
|
||||
import { IRSEditContext } from '../RSEditContext';
|
||||
|
||||
interface ToolbarRSFormCardProps {
|
||||
modified: boolean;
|
||||
onSubmit: () => void;
|
||||
onDestroy: () => void;
|
||||
controller: ILibraryItemEditor;
|
||||
}
|
||||
|
||||
function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: ToolbarRSFormCardProps) {
|
||||
function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardProps) {
|
||||
const role = useRoleStore(state => state.role);
|
||||
const canSave = modified && !controller.isProcessing;
|
||||
const { isModified } = useModificationStore();
|
||||
const isProcessing = useIsProcessingLibrary();
|
||||
const canSave = isModified && !isProcessing;
|
||||
|
||||
const ossSelector = (() => {
|
||||
if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) {
|
||||
|
@ -37,7 +40,9 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
|
|||
return (
|
||||
<MiniSelectorOSS
|
||||
items={schema.oss}
|
||||
onSelect={(event, value) => (controller as IRSEditContext).viewOSS(value.id, event.ctrlKey || event.metaKey)}
|
||||
onSelect={(event, value) =>
|
||||
(controller as IRSEditContext).navigateOss(value.id, event.ctrlKey || event.metaKey)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
@ -45,7 +50,7 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
|
|||
return (
|
||||
<Overlay position='cc-tab-tools' className='cc-icons'>
|
||||
{ossSelector}
|
||||
{controller.isMutable || modified ? (
|
||||
{controller.isMutable || isModified ? (
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||
disabled={!canSave}
|
||||
|
@ -56,15 +61,15 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
|
|||
<MiniButton
|
||||
titleHtml={tooltips.shareItem(controller.schema?.access_policy)}
|
||||
icon={<IconShare size='1.25rem' className='icon-primary' />}
|
||||
onClick={controller.share}
|
||||
onClick={sharePage}
|
||||
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
|
||||
/>
|
||||
{controller.isMutable ? (
|
||||
<MiniButton
|
||||
title='Удалить схему'
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
disabled={!controller.isMutable || controller.isProcessing || role < UserRole.OWNER}
|
||||
onClick={onDestroy}
|
||||
disabled={!controller.isMutable || isProcessing || role < UserRole.OWNER}
|
||||
onClick={controller.deleteSchema}
|
||||
/>
|
||||
) : null}
|
||||
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} className={PARAMETER.TOOLTIP_WIDTH} />
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useVersionCreate } from '@/backend/library/useVersionCreate';
|
||||
import { useVersionRestore } from '@/backend/library/useVersionRestore';
|
||||
import { IconNewVersion, IconUpload, IconVersions } from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { information, prompts } from '@/utils/labels';
|
||||
import { promptUnsaved } from '@/utils/utils';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
|
@ -13,6 +23,45 @@ interface ToolbarVersioningProps {
|
|||
|
||||
function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
|
||||
const controller = useRSEdit();
|
||||
const router = useConceptNavigation();
|
||||
const { isModified } = useModificationStore();
|
||||
const { versionRestore } = useVersionRestore();
|
||||
const { versionCreate } = useVersionCreate();
|
||||
|
||||
const showCreateVersion = useDialogsStore(state => state.showCreateVersion);
|
||||
const showEditVersions = useDialogsStore(state => state.showEditVersions);
|
||||
|
||||
function handleRestoreVersion() {
|
||||
if (!controller.schema.version || !window.confirm(prompts.restoreArchive)) {
|
||||
return;
|
||||
}
|
||||
versionRestore({ itemID: controller.schema.id, versionID: controller.schema.version }, () => {
|
||||
toast.success(information.versionRestored);
|
||||
router.push(urls.schema(controller.schema.id));
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateVersion() {
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
showCreateVersion({
|
||||
versions: controller.schema.versions,
|
||||
selected: controller.selected,
|
||||
totalCount: controller.schema.items.length,
|
||||
onCreate: data =>
|
||||
versionCreate({ itemID: controller.schema.id, data: data }, () => {
|
||||
toast.success(information.newVersion(data.version));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditVersions() {
|
||||
showEditVersions({
|
||||
item: controller.schema
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons' layer='z-bottom'>
|
||||
{controller.isMutable ? (
|
||||
|
@ -26,19 +75,19 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
|
|||
: 'Переключитесь на <br/>неактуальную версию'
|
||||
}
|
||||
disabled={controller.isContentEditable || blockReload}
|
||||
onClick={() => controller.restoreVersion()}
|
||||
onClick={handleRestoreVersion}
|
||||
icon={<IconUpload size='1.25rem' className='icon-red' />}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
|
||||
disabled={!controller.isContentEditable}
|
||||
onClick={controller.createVersion}
|
||||
onClick={handleCreateVersion}
|
||||
icon={<IconNewVersion size='1.25rem' className='icon-green' />}
|
||||
/>
|
||||
<MiniButton
|
||||
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
||||
disabled={!controller.schema || controller.schema?.versions.length === 0}
|
||||
onClick={controller.promptEditVersions}
|
||||
onClick={handleEditVersions}
|
||||
icon={<IconVersions size='1.25rem' className='icon-primary' />}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -4,6 +4,7 @@ import fileDownload from 'js-file-download';
|
|||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import { IconCSV } from '@/components/Icons';
|
||||
import { type RowSelectionState } from '@/components/ui/DataTable';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
|
@ -20,13 +21,10 @@ import { useRSEdit } from '../RSEditContext';
|
|||
import TableRSList from './TableRSList';
|
||||
import ToolbarRSList from './ToolbarRSList';
|
||||
|
||||
interface EditorRSListProps {
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||
function EditorRSList() {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const controller = useRSEdit();
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
|
||||
const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema?.items ?? []);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
|
@ -91,7 +89,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
controller.deselectAll();
|
||||
return;
|
||||
}
|
||||
if (!controller.isContentEditable || controller.isProcessing) {
|
||||
if (!controller.isContentEditable || isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Delete' && controller.canDeleteSelected) {
|
||||
|
@ -170,7 +168,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
enableSelection={controller.isContentEditable}
|
||||
selected={rowSelection}
|
||||
setSelected={handleRowSelection}
|
||||
onEdit={onOpenEdit}
|
||||
onEdit={controller.navigateCst}
|
||||
onCreateNew={() => controller.createCst(undefined, false)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import { CstTypeIcon } from '@/components/DomainIcons';
|
||||
import {
|
||||
IconClone,
|
||||
|
@ -24,6 +25,7 @@ import { useRSEdit } from '../RSEditContext';
|
|||
|
||||
function ToolbarRSList() {
|
||||
const controller = useRSEdit();
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
const insertMenu = useDropdown();
|
||||
|
||||
return (
|
||||
|
@ -34,7 +36,7 @@ function ToolbarRSList() {
|
|||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||
<MiniSelectorOSS
|
||||
items={controller.schema.oss}
|
||||
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
|
||||
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
|
||||
/>
|
||||
) : null}
|
||||
<MiniButton
|
||||
|
@ -47,7 +49,7 @@ function ToolbarRSList() {
|
|||
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
||||
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
|
||||
disabled={
|
||||
controller.isProcessing ||
|
||||
isProcessing ||
|
||||
controller.selected.length === 0 ||
|
||||
(controller.schema && controller.selected.length === controller.schema.items.length)
|
||||
}
|
||||
|
@ -57,7 +59,7 @@ function ToolbarRSList() {
|
|||
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
|
||||
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
|
||||
disabled={
|
||||
controller.isProcessing ||
|
||||
isProcessing ||
|
||||
controller.selected.length === 0 ||
|
||||
(controller.schema && controller.selected.length === controller.schema.items.length)
|
||||
}
|
||||
|
@ -68,7 +70,7 @@ function ToolbarRSList() {
|
|||
title='Добавить пустую конституенту'
|
||||
hideTitle={insertMenu.isOpen}
|
||||
icon={<IconOpenList size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={isProcessing}
|
||||
onClick={insertMenu.toggle}
|
||||
/>
|
||||
<Dropdown isOpen={insertMenu.isOpen} className='-translate-x-1/2 md:translate-x-0'>
|
||||
|
@ -86,19 +88,19 @@ function ToolbarRSList() {
|
|||
<MiniButton
|
||||
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={isProcessing}
|
||||
onClick={() => controller.createCst(undefined, false)}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
||||
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing || controller.selected.length !== 1}
|
||||
disabled={isProcessing || controller.selected.length !== 1}
|
||||
onClick={controller.cloneCst}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
disabled={controller.isProcessing || !controller.canDeleteSelected}
|
||||
disabled={isProcessing || !controller.canDeleteSelected}
|
||||
onClick={controller.promptDeleteCst}
|
||||
/>
|
||||
<BadgeHelp topic={HelpTopic.UI_RS_LIST} offset={5} />
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
import { ConstituentaID } from '@/models/rsform';
|
||||
|
||||
import TGFlow from './TGFlow';
|
||||
|
||||
interface EditorTermGraphProps {
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||
function EditorTermGraph() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<TGFlow onOpenEdit={onOpenEdit} />
|
||||
<TGFlow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
import { useStoreApi } from 'reactflow';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
||||
import SelectedCounter from '@/components/info/SelectedCounter';
|
||||
import { CProps } from '@/components/props';
|
||||
|
@ -47,18 +48,13 @@ import ViewHidden from './ViewHidden';
|
|||
const ZOOM_MAX = 3;
|
||||
const ZOOM_MIN = 0.25;
|
||||
|
||||
interface TGFlowProps {
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||
function TGFlow() {
|
||||
const mainHeight = useMainHeight();
|
||||
const controller = useRSEdit();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges] = useEdgesState([]);
|
||||
const flow = useReactFlow();
|
||||
const store = useStoreApi();
|
||||
const { addSelectedNodes } = store.getState();
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
|
||||
const showParams = useDialogsStore(state => state.showGraphParams);
|
||||
|
||||
|
@ -67,6 +63,9 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
|||
const coloring = useTermGraphStore(state => state.coloring);
|
||||
const setColoring = useTermGraphStore(state => state.setColoring);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges] = useEdgesState([]);
|
||||
|
||||
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
||||
const filteredGraph = useGraphFilter(controller.schema, filter, focusCst);
|
||||
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
||||
|
@ -113,7 +112,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
|||
const resetNodes = useCallback(() => {
|
||||
const newNodes: Node<TGNodeData>[] = [];
|
||||
filteredGraph.nodes.forEach(node => {
|
||||
const cst = controller.schema!.cstByID.get(node.id);
|
||||
const cst = controller.schema.cstByID.get(node.id);
|
||||
if (cst) {
|
||||
newNodes.push({
|
||||
id: String(node.id),
|
||||
|
@ -184,7 +183,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
|||
if (!controller.schema) {
|
||||
return;
|
||||
}
|
||||
const definition = controller.selected.map(id => controller.schema!.cstByID.get(id)!.alias).join(' ');
|
||||
const definition = controller.selected.map(id => controller.schema.cstByID.get(id)!.alias).join(' ');
|
||||
controller.createCst(controller.selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||
}
|
||||
|
||||
|
@ -232,7 +231,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
|||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (controller.isProcessing) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
|
@ -282,7 +281,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
|||
function handleNodeDoubleClick(event: CProps.EventMouse, cstID: ConstituentaID) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onOpenEdit(cstID);
|
||||
controller.navigateCst(cstID);
|
||||
}
|
||||
|
||||
function handleNodeEnter(event: CProps.EventMouse, cstID: ConstituentaID) {
|
||||
|
@ -314,11 +313,11 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
|||
/>
|
||||
{!focusCst ? (
|
||||
<ToolbarGraphSelection
|
||||
graph={controller.schema!.graph}
|
||||
graph={controller.schema.graph}
|
||||
isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
|
||||
isOwned={
|
||||
controller.schema && controller.schema.inheritance.length > 0
|
||||
? cstID => !controller.schema!.cstByID.get(cstID)?.is_inherited
|
||||
? cstID => !controller.schema.cstByID.get(cstID)?.is_inherited
|
||||
: undefined
|
||||
}
|
||||
selected={controller.selected}
|
||||
|
@ -386,7 +385,6 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
|||
coloringScheme={coloring}
|
||||
toggleSelection={controller.toggleSelect}
|
||||
setFocus={handleSetFocus}
|
||||
onEdit={onOpenEdit}
|
||||
/>
|
||||
</div>
|
||||
</Overlay>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import {
|
||||
IconClustering,
|
||||
IconClusteringOff,
|
||||
|
@ -16,6 +17,7 @@ import BadgeHelp from '@/components/info/BadgeHelp';
|
|||
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
@ -46,13 +48,24 @@ function ToolbarTermGraph({
|
|||
onSaveImage
|
||||
}: ToolbarTermGraphProps) {
|
||||
const controller = useRSEdit();
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
|
||||
|
||||
function handleShowTypeGraph() {
|
||||
const typeInfo = controller.schema?.items.map(item => ({
|
||||
alias: item.alias,
|
||||
result: item.parse.typification,
|
||||
args: item.parse.args
|
||||
}));
|
||||
showTypeGraph({ items: typeInfo ?? [] });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='cc-icons'>
|
||||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||
<MiniSelectorOSS
|
||||
items={controller.schema.oss}
|
||||
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
|
||||
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
|
||||
/>
|
||||
) : null}
|
||||
<MiniButton
|
||||
|
@ -91,7 +104,7 @@ function ToolbarTermGraph({
|
|||
<MiniButton
|
||||
title='Новая конституента'
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={isProcessing}
|
||||
onClick={onCreate}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -99,14 +112,14 @@ function ToolbarTermGraph({
|
|||
<MiniButton
|
||||
title='Удалить выбранные'
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
disabled={!controller.canDeleteSelected || controller.isProcessing}
|
||||
disabled={!controller.canDeleteSelected || isProcessing}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
) : null}
|
||||
<MiniButton
|
||||
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
|
||||
title='Граф ступеней'
|
||||
onClick={() => controller.showTypeGraph()}
|
||||
onClick={handleShowTypeGraph}
|
||||
/>
|
||||
<MiniButton
|
||||
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
||||
|
|
|
@ -15,6 +15,8 @@ import { useTooltipsStore } from '@/stores/tooltips';
|
|||
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
|
||||
import { globals, PARAMETER, prefixes } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface ViewHiddenProps {
|
||||
items: ConstituentaID[];
|
||||
selected: ConstituentaID[];
|
||||
|
@ -23,13 +25,13 @@ interface ViewHiddenProps {
|
|||
|
||||
toggleSelection: (cstID: ConstituentaID) => void;
|
||||
setFocus: (cstID: ConstituentaID) => void;
|
||||
onEdit: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) {
|
||||
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme }: ViewHiddenProps) {
|
||||
const windowSize = useWindowSize();
|
||||
const localSelected = items.filter(id => selected.includes(id));
|
||||
|
||||
const { navigateCst } = useRSEdit();
|
||||
const isFolded = useTermGraphStore(state => state.foldHidden);
|
||||
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
|
||||
const setActiveCst = useTooltipsStore(state => state.setActiveCst);
|
||||
|
@ -108,7 +110,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
|||
: {})
|
||||
}}
|
||||
onClick={event => handleClick(cstID, event)}
|
||||
onDoubleClick={() => onEdit(cstID)}
|
||||
onDoubleClick={() => navigateCst(cstID)}
|
||||
data-tooltip-id={globals.constituenta_tooltip}
|
||||
onMouseEnter={() => setActiveCst(cst)}
|
||||
>
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
'use client';
|
||||
|
||||
import { useGlobalOss } from '@/app/GlobalOssContext';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute';
|
||||
import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm';
|
||||
import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis';
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import { useProduceStructure } from '@/backend/rsform/useProduceStructure';
|
||||
import { useResetAliases } from '@/backend/rsform/useResetAliases';
|
||||
import { useRestoreOrder } from '@/backend/rsform/useRestoreOrder';
|
||||
import {
|
||||
IconAdmin,
|
||||
IconAlert,
|
||||
|
@ -19,7 +28,6 @@ import {
|
|||
IconLibrary,
|
||||
IconMenu,
|
||||
IconNewItem,
|
||||
IconNewVersion,
|
||||
IconOSS,
|
||||
IconOwner,
|
||||
IconQR,
|
||||
|
@ -35,79 +43,142 @@ import Divider from '@/components/ui/Divider';
|
|||
import Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { AccessPolicy } from '@/models/library';
|
||||
import { AccessPolicy, LocationHead } from '@/models/library';
|
||||
import { CstType } from '@/models/rsform';
|
||||
import { UserRole } from '@/models/user';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { useRoleStore } from '@/stores/role';
|
||||
import { describeAccessMode, labelAccessMode, tooltips } from '@/utils/labels';
|
||||
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
||||
import { describeAccessMode, information, labelAccessMode, tooltips } from '@/utils/labels';
|
||||
import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils';
|
||||
|
||||
import { OssTabID } from '../OssPage/OssTabs';
|
||||
import { OssTabID } from '../OssPage/OssEditContext';
|
||||
import { useRSEdit } from './RSEditContext';
|
||||
|
||||
interface MenuRSTabsProps {
|
||||
onDestroy: () => void;
|
||||
}
|
||||
|
||||
function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||
function MenuRSTabs() {
|
||||
const controller = useRSEdit();
|
||||
const router = useConceptNavigation();
|
||||
const { user } = useAuth();
|
||||
const oss = useGlobalOss();
|
||||
|
||||
const role = useRoleStore(state => state.role);
|
||||
const setRole = useRoleStore(state => state.setRole);
|
||||
const { isModified } = useModificationStore();
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
|
||||
const { resetAliases } = useResetAliases();
|
||||
const { restoreOrder } = useRestoreOrder();
|
||||
const { produceStructure } = useProduceStructure();
|
||||
const { inlineSynthesis } = useInlineSynthesis();
|
||||
const { cstSubstitute } = useCstSubstitute();
|
||||
const { download } = useDownloadRSForm();
|
||||
|
||||
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
|
||||
const showQR = useDialogsStore(state => state.showQR);
|
||||
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
|
||||
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
|
||||
const showUpload = useDialogsStore(state => state.showUploadRSForm);
|
||||
|
||||
const schemaMenu = useDropdown();
|
||||
const editMenu = useDropdown();
|
||||
const accessMenu = useDropdown();
|
||||
|
||||
// TODO: move into separate function
|
||||
const canProduceStructure =
|
||||
!!controller.activeCst &&
|
||||
!!controller.activeCst.parse.typification &&
|
||||
controller.activeCst.cst_type !== CstType.BASE &&
|
||||
controller.activeCst.cst_type !== CstType.CONSTANT;
|
||||
|
||||
function calculateCloneLocation() {
|
||||
const location = controller.schema.location;
|
||||
const head = location.substring(0, 2) as LocationHead;
|
||||
if (head === LocationHead.LIBRARY) {
|
||||
return user?.is_staff ? location : LocationHead.USER;
|
||||
}
|
||||
if (controller.schema.owner === user?.id) {
|
||||
return location;
|
||||
}
|
||||
return head === LocationHead.USER ? LocationHead.USER : location;
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
schemaMenu.hide();
|
||||
onDestroy();
|
||||
controller.deleteSchema();
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
schemaMenu.hide();
|
||||
controller.download();
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
const fileName = (controller.schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
|
||||
download({ itemID: controller.schema.id, version: controller.schema.version }, (data: Blob) => {
|
||||
try {
|
||||
fileDownload(data, fileName);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
schemaMenu.hide();
|
||||
controller.promptUpload();
|
||||
showUpload({ itemID: controller.schema.id });
|
||||
}
|
||||
|
||||
function handleClone() {
|
||||
schemaMenu.hide();
|
||||
controller.promptClone();
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
showClone({
|
||||
base: controller.schema,
|
||||
initialLocation: calculateCloneLocation(),
|
||||
selected: controller.selected,
|
||||
totalCount: controller.schema.items.length
|
||||
});
|
||||
}
|
||||
|
||||
function handleShare() {
|
||||
schemaMenu.hide();
|
||||
controller.share();
|
||||
sharePage();
|
||||
}
|
||||
|
||||
function handleShowQR() {
|
||||
schemaMenu.hide();
|
||||
controller.showQR();
|
||||
}
|
||||
|
||||
function handleCreateVersion() {
|
||||
schemaMenu.hide();
|
||||
controller.createVersion();
|
||||
showQR({ target: generatePageQR() });
|
||||
}
|
||||
|
||||
function handleReindex() {
|
||||
editMenu.hide();
|
||||
controller.reindex();
|
||||
resetAliases(controller.schema.id, () => toast.success(information.reindexComplete));
|
||||
}
|
||||
|
||||
function handleRestoreOrder() {
|
||||
editMenu.hide();
|
||||
controller.reorder();
|
||||
restoreOrder(controller.schema.id, () => toast.success(information.reorderComplete));
|
||||
}
|
||||
|
||||
function handleSubstituteCst() {
|
||||
editMenu.hide();
|
||||
controller.substitute();
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
showSubstituteCst({
|
||||
schema: controller.schema,
|
||||
onSubstitute: data =>
|
||||
cstSubstitute(
|
||||
{
|
||||
itemID: controller.schema.id, //
|
||||
data
|
||||
},
|
||||
() => {
|
||||
controller.setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)));
|
||||
toast.success(information.substituteSingle);
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
function handleTemplates() {
|
||||
|
@ -117,12 +188,41 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
|
||||
function handleProduceStructure() {
|
||||
editMenu.hide();
|
||||
controller.produceStructure();
|
||||
if (!controller.activeCst) {
|
||||
return;
|
||||
}
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
produceStructure(
|
||||
{
|
||||
itemID: controller.schema.id, //
|
||||
data: { target: controller.activeCst.id }
|
||||
},
|
||||
cstList => {
|
||||
toast.success(information.addedConstituents(cstList.length));
|
||||
if (cstList.length !== 0) {
|
||||
controller.setSelected(cstList);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleInlineSynthesis() {
|
||||
editMenu.hide();
|
||||
controller.inlineSynthesis();
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
showInlineSynthesis({
|
||||
receiver: controller.schema,
|
||||
onInlineSynthesis: data => {
|
||||
const oldCount = controller.schema.items.length;
|
||||
inlineSynthesis({ itemID: controller.schema.id, data }, newSchema => {
|
||||
controller.deselectAll();
|
||||
toast.success(information.addedConstituents(newSchema.items.length - oldCount));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleChangeMode(newMode: UserRole) {
|
||||
|
@ -155,7 +255,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
<Dropdown isOpen={schemaMenu.isOpen}>
|
||||
<DropdownButton
|
||||
text='Поделиться'
|
||||
titleHtml={tooltips.shareItem(controller.schema?.access_policy)}
|
||||
titleHtml={tooltips.shareItem(controller.schema.access_policy)}
|
||||
icon={<IconShare size='1rem' className='icon-primary' />}
|
||||
onClick={handleShare}
|
||||
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
|
||||
|
@ -174,12 +274,6 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
onClick={handleClone}
|
||||
/>
|
||||
) : null}
|
||||
<DropdownButton
|
||||
text='Сохранить версию'
|
||||
disabled={!controller.isContentEditable}
|
||||
onClick={handleCreateVersion}
|
||||
icon={<IconNewVersion size='1rem' className='icon-green' />}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Выгрузить в Экстеор'
|
||||
icon={<IconDownload size='1rem' className='icon-primary' />}
|
||||
|
@ -189,7 +283,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
<DropdownButton
|
||||
text='Загрузить из Экстеор'
|
||||
icon={<IconUpload size='1rem' className='icon-red' />}
|
||||
disabled={controller.isProcessing || controller.schema?.oss.length !== 0}
|
||||
disabled={isProcessing || controller.schema?.oss.length !== 0}
|
||||
onClick={handleUpload}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -197,7 +291,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
<DropdownButton
|
||||
text='Удалить схему'
|
||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||
disabled={controller.isProcessing || role < UserRole.OWNER}
|
||||
disabled={isProcessing || role < UserRole.OWNER}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -211,11 +305,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
onClick={handleCreateNew}
|
||||
/>
|
||||
) : null}
|
||||
{oss.schema ? (
|
||||
{controller.schema.oss.length > 0 ? (
|
||||
<DropdownButton
|
||||
text='Перейти к ОСС'
|
||||
icon={<IconOSS size='1rem' className='icon-primary' />}
|
||||
onClick={() => router.push(urls.oss(oss.schema!.id, OssTabID.GRAPH))}
|
||||
onClick={() => router.push(urls.oss(controller.schema.oss[0].id, OssTabID.GRAPH))}
|
||||
/>
|
||||
) : null}
|
||||
<DropdownButton
|
||||
|
@ -225,7 +319,6 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{!controller.isArchive && user ? (
|
||||
<div ref={editMenu.ref}>
|
||||
<Button
|
||||
|
@ -244,14 +337,14 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
text='Шаблоны'
|
||||
title='Создать конституенту из шаблона'
|
||||
icon={<IconTemplates size='1rem' className='icon-green' />}
|
||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
||||
disabled={!controller.isContentEditable || isProcessing}
|
||||
onClick={handleTemplates}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Встраивание'
|
||||
titleHtml='Импортировать совокупность <br/>конституент из другой схемы'
|
||||
icon={<IconInlineSynthesis size='1rem' className='icon-green' />}
|
||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
||||
disabled={!controller.isContentEditable || isProcessing}
|
||||
onClick={handleInlineSynthesis}
|
||||
/>
|
||||
|
||||
|
@ -261,21 +354,21 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
text='Упорядочить список'
|
||||
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
|
||||
icon={<IconSortList size='1rem' className='icon-primary' />}
|
||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
||||
disabled={!controller.isContentEditable || isProcessing}
|
||||
onClick={handleRestoreOrder}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Порядковые имена'
|
||||
titleHtml='Присвоить порядковые имена <br/>и обновить выражения'
|
||||
icon={<IconGenerateNames size='1rem' className='icon-primary' />}
|
||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
||||
disabled={!controller.isContentEditable || isProcessing}
|
||||
onClick={handleReindex}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Порождение структуры'
|
||||
titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты'
|
||||
icon={<IconGenerateStructure size='1rem' className='icon-primary' />}
|
||||
disabled={!controller.isContentEditable || !controller.canProduceStructure || controller.isProcessing}
|
||||
disabled={!controller.isContentEditable || !canProduceStructure || isProcessing}
|
||||
onClick={handleProduceStructure}
|
||||
/>
|
||||
<DropdownButton
|
||||
|
@ -283,11 +376,12 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
titleHtml='Заменить вхождения <br/>одной конституенты на другую'
|
||||
icon={<IconReplace size='1rem' className='icon-red' />}
|
||||
onClick={handleSubstituteCst}
|
||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
||||
disabled={!controller.isContentEditable || isProcessing}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
) : null}
|
||||
router.push(urls.schema(itemID, version), newTab);
|
||||
{controller.isArchive && user ? (
|
||||
<Button
|
||||
dense
|
||||
|
@ -298,10 +392,9 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
hideTitle={accessMenu.isOpen}
|
||||
className='h-full px-2'
|
||||
icon={<IconArchive size='1.25rem' className='icon-primary' />}
|
||||
onClick={event => controller.viewVersion(undefined, event.ctrlKey || event.metaKey)}
|
||||
onClick={event => router.push(urls.schema(controller.schema.id), event.ctrlKey || event.metaKey)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{user ? (
|
||||
<div ref={accessMenu.ref}>
|
||||
<Button
|
||||
|
|
|
@ -1,69 +1,55 @@
|
|||
'use client';
|
||||
|
||||
import fileDownload from 'js-file-download';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
|
||||
import { useSetEditors } from '@/backend/library/useSetEditors';
|
||||
import { useSetLocation } from '@/backend/library/useSetLocation';
|
||||
import { useSetOwner } from '@/backend/library/useSetOwner';
|
||||
import { useFindPredecessor } from '@/backend/oss/useFindPredecessor';
|
||||
import { ICstCreateDTO, ICstRenameDTO, ICstUpdateDTO, IInlineSynthesisDTO } from '@/backend/rsform/api';
|
||||
import { useDeleteItem } from '@/backend/library/useDeleteItem';
|
||||
import { ICstCreateDTO } from '@/backend/rsform/api';
|
||||
import { useCstCreate } from '@/backend/rsform/useCstCreate';
|
||||
import { useCstDelete } from '@/backend/rsform/useCstDelete';
|
||||
import { useCstMove } from '@/backend/rsform/useCstMove';
|
||||
import { useCstRename } from '@/backend/rsform/useCstRename';
|
||||
import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute';
|
||||
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
|
||||
import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm';
|
||||
import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis';
|
||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||
import { useProduceStructure } from '@/backend/rsform/useProduceStructure';
|
||||
import { useResetAliases } from '@/backend/rsform/useResetAliases';
|
||||
import { useRestoreOrder } from '@/backend/rsform/useRestoreOrder';
|
||||
import { useRSFormSuspense } from '@/backend/rsform/useRSForm';
|
||||
import {
|
||||
AccessPolicy,
|
||||
ILibraryItemEditor,
|
||||
IVersionData,
|
||||
LibraryItemID,
|
||||
LocationHead,
|
||||
VersionID
|
||||
} from '@/models/library';
|
||||
import { ICstSubstitutions } from '@/models/oss';
|
||||
import { ConstituentaID, CstType, IConstituenta, IConstituentaMeta, IRSForm, TermForm } from '@/models/rsform';
|
||||
import { ILibraryItemEditor, LibraryItemID, VersionID } from '@/models/library';
|
||||
import { ConstituentaID, CstType, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import { generateAlias } from '@/models/rsformAPI';
|
||||
import { UserID, UserRole } from '@/models/user';
|
||||
import { UserRole } from '@/models/user';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { useRoleStore } from '@/stores/role';
|
||||
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
import { information, prompts } from '@/utils/labels';
|
||||
import { promptUnsaved } from '@/utils/utils';
|
||||
|
||||
import { RSTabID } from './RSTabs';
|
||||
import { OssTabID } from '../OssPage/OssEditContext';
|
||||
|
||||
export enum RSTabID {
|
||||
CARD = 0,
|
||||
CST_LIST = 1,
|
||||
CST_EDIT = 2,
|
||||
TERM_GRAPH = 3
|
||||
}
|
||||
|
||||
export interface IRSEditContext extends ILibraryItemEditor {
|
||||
schema?: IRSForm;
|
||||
schema: IRSForm;
|
||||
selected: ConstituentaID[];
|
||||
activeCst?: IConstituenta;
|
||||
|
||||
isOwned: boolean;
|
||||
isArchive: boolean;
|
||||
isMutable: boolean;
|
||||
isContentEditable: boolean;
|
||||
isProcessing: boolean;
|
||||
isAttachedToOSS: boolean;
|
||||
canProduceStructure: boolean;
|
||||
canDeleteSelected: boolean;
|
||||
|
||||
setOwner: (newOwner: UserID) => void;
|
||||
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
||||
promptEditors: () => void;
|
||||
promptLocation: () => void;
|
||||
navigateRSForm: ({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) => void;
|
||||
navigateCst: (cstID: ConstituentaID) => void;
|
||||
navigateOss: (target: LibraryItemID, newTab?: boolean) => void;
|
||||
|
||||
deleteSchema: () => void;
|
||||
|
||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||
select: (target: ConstituentaID) => void;
|
||||
|
@ -71,35 +57,12 @@ export interface IRSEditContext extends ILibraryItemEditor {
|
|||
toggleSelect: (target: ConstituentaID) => void;
|
||||
deselectAll: () => void;
|
||||
|
||||
viewOSS: (target: LibraryItemID, newTab?: boolean) => void;
|
||||
viewVersion: (version?: VersionID, newTab?: boolean) => void;
|
||||
viewPredecessor: (target: ConstituentaID) => void;
|
||||
createVersion: () => void;
|
||||
restoreVersion: () => void;
|
||||
promptEditVersions: () => void;
|
||||
|
||||
moveUp: () => void;
|
||||
moveDown: () => void;
|
||||
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
|
||||
renameCst: () => void;
|
||||
cloneCst: () => void;
|
||||
promptDeleteCst: () => void;
|
||||
editTermForms: () => void;
|
||||
|
||||
promptTemplate: () => void;
|
||||
promptClone: () => void;
|
||||
promptUpload: () => void;
|
||||
share: () => void;
|
||||
download: () => void;
|
||||
|
||||
reindex: () => void;
|
||||
reorder: () => void;
|
||||
produceStructure: () => void;
|
||||
inlineSynthesis: () => void;
|
||||
substitute: () => void;
|
||||
|
||||
showTypeGraph: () => void;
|
||||
showQR: () => void;
|
||||
}
|
||||
|
||||
const RSEditContext = createContext<IRSEditContext | null>(null);
|
||||
|
@ -113,28 +76,11 @@ export const useRSEdit = () => {
|
|||
|
||||
interface RSEditStateProps {
|
||||
itemID: LibraryItemID;
|
||||
activeTab: RSTabID;
|
||||
versionID?: VersionID;
|
||||
|
||||
selected: ConstituentaID[];
|
||||
isModified: boolean;
|
||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||
activeCst?: IConstituenta;
|
||||
|
||||
onCreateCst?: (newCst: IConstituentaMeta) => void;
|
||||
onDeleteCst?: (newActive?: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
export const RSEditState = ({
|
||||
itemID,
|
||||
versionID,
|
||||
selected,
|
||||
setSelected,
|
||||
activeCst,
|
||||
isModified,
|
||||
onCreateCst,
|
||||
onDeleteCst,
|
||||
children
|
||||
}: React.PropsWithChildren<RSEditStateProps>) => {
|
||||
export const RSEditState = ({ itemID, versionID, activeTab, children }: React.PropsWithChildren<RSEditStateProps>) => {
|
||||
const router = useConceptNavigation();
|
||||
const { user } = useAuth();
|
||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||
|
@ -142,72 +88,33 @@ export const RSEditState = ({
|
|||
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||
|
||||
const { schema } = useRSFormSuspense({ itemID: itemID, version: versionID });
|
||||
const isProcessing = useIsProcessingRSForm();
|
||||
|
||||
const { download: downloadFile } = useDownloadRSForm();
|
||||
const { findPredecessor } = useFindPredecessor();
|
||||
|
||||
const { setOwner: setItemOwner } = useSetOwner();
|
||||
const { setLocation: setItemLocation } = useSetLocation();
|
||||
const { setAccessPolicy: setItemAccessPolicy } = useSetAccessPolicy();
|
||||
const { setEditors: setItemEditors } = useSetEditors();
|
||||
|
||||
const { cstCreate } = useCstCreate();
|
||||
const { cstRename } = useCstRename();
|
||||
const { cstSubstitute } = useCstSubstitute();
|
||||
const { cstMove } = useCstMove();
|
||||
const { cstDelete } = useCstDelete();
|
||||
const { cstUpdate } = useCstUpdate();
|
||||
|
||||
const { produceStructure: produceStructureInternal } = useProduceStructure();
|
||||
const { inlineSynthesis: inlineSynthesisInternal } = useInlineSynthesis();
|
||||
const { restoreOrder: restoreOrderInternal } = useRestoreOrder();
|
||||
const { resetAliases: resetAliasesInternal } = useResetAliases();
|
||||
const { isModified } = useModificationStore();
|
||||
|
||||
const isOwned = user?.id === schema?.owner || false;
|
||||
|
||||
const isArchive = !!versionID;
|
||||
|
||||
const isMutable = role > UserRole.READER && !schema?.read_only;
|
||||
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||
const isContentEditable = isMutable && !isArchive;
|
||||
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema?.cstByID.get(id)?.is_inherited);
|
||||
const isAttachedToOSS =
|
||||
!!schema && schema.oss.length > 0 && (schema.stats.count_inherited > 0 || schema.items.length === 0);
|
||||
const isAttachedToOSS = schema.oss.length > 0;
|
||||
|
||||
const [renameInitialData, setRenameInitialData] = useState<ICstRenameDTO>();
|
||||
const [selected, setSelected] = useState<ConstituentaID[]>([]);
|
||||
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
|
||||
|
||||
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
|
||||
const showCreateVersion = useDialogsStore(state => state.showCreateVersion);
|
||||
const showEditVersions = useDialogsStore(state => state.showEditVersions);
|
||||
const showEditEditors = useDialogsStore(state => state.showEditEditors);
|
||||
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
|
||||
const activeCst: IConstituenta | undefined = (() => {
|
||||
if (!schema || selected.length === 0) {
|
||||
return undefined;
|
||||
} else {
|
||||
return schema.cstByID.get(selected[-1]);
|
||||
}
|
||||
})();
|
||||
|
||||
const { cstCreate } = useCstCreate();
|
||||
const { cstMove } = useCstMove();
|
||||
const { cstDelete } = useCstDelete();
|
||||
const { deleteItem } = useDeleteItem();
|
||||
|
||||
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
||||
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
||||
|
||||
const showRenameCst = useDialogsStore(state => state.showRenameCst);
|
||||
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
|
||||
|
||||
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
|
||||
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
|
||||
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
|
||||
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
|
||||
const showUpload = useDialogsStore(state => state.showUploadRSForm);
|
||||
const showQR = useDialogsStore(state => state.showQR);
|
||||
|
||||
const typeInfo = schema
|
||||
? schema.items.map(item => ({
|
||||
alias: item.alias,
|
||||
result: item.parse.typification,
|
||||
args: item.parse.args
|
||||
}))
|
||||
: [];
|
||||
|
||||
const canProduceStructure =
|
||||
!!activeCst &&
|
||||
!!activeCst.parse.typification &&
|
||||
activeCst.cst_type !== CstType.BASE &&
|
||||
activeCst.cst_type !== CstType.CONSTANT;
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
|
@ -220,79 +127,78 @@ export const RSEditState = ({
|
|||
[schema, adjustRole, isOwned, user, adminMode]
|
||||
);
|
||||
|
||||
function viewVersion(version?: VersionID, newTab?: boolean) {
|
||||
router.push(urls.schema(itemID, version), newTab);
|
||||
}
|
||||
|
||||
function viewPredecessor(target: ConstituentaID) {
|
||||
findPredecessor({ target: target }, reference =>
|
||||
router.push(
|
||||
urls.schema_props({
|
||||
id: reference.schema,
|
||||
active: reference.id,
|
||||
tab: RSTabID.CST_EDIT
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function viewOSS(target: LibraryItemID, newTab?: boolean) {
|
||||
function navigateOss(target: LibraryItemID, newTab?: boolean) {
|
||||
router.push(urls.oss(target), newTab);
|
||||
}
|
||||
|
||||
function restoreVersion() {
|
||||
if (!versionID || !window.confirm(prompts.restoreArchive)) {
|
||||
function navigateRSForm({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
model.versionRestore(versionID, () => {
|
||||
toast.success(information.versionRestored);
|
||||
viewVersion(undefined);
|
||||
const data = {
|
||||
id: schema.id,
|
||||
tab: tab,
|
||||
active: activeID,
|
||||
version: versionID
|
||||
};
|
||||
const url = urls.schema_props(data);
|
||||
if (activeID) {
|
||||
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
|
||||
router.replace(url);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
|
||||
data.active = schema.items[0].id;
|
||||
router.replace(urls.schema_props(data));
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateCst(cstID: ConstituentaID) {
|
||||
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
|
||||
navigateRSForm({ tab: RSTabID.CST_EDIT, activeID: cstID });
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSchema() {
|
||||
if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
|
||||
return;
|
||||
}
|
||||
const ossID = schema.oss.length > 0 ? schema.oss[0].id : undefined;
|
||||
deleteItem(schema.id, () => {
|
||||
toast.success(information.itemDestroyed);
|
||||
if (ossID) {
|
||||
router.push(urls.oss(ossID, OssTabID.GRAPH));
|
||||
} else {
|
||||
router.push(urls.library);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function calculateCloneLocation() {
|
||||
if (!schema) {
|
||||
return LocationHead.USER;
|
||||
}
|
||||
const location = schema.location;
|
||||
const head = schema.location.substring(0, 2) as LocationHead;
|
||||
if (head === LocationHead.LIBRARY) {
|
||||
return user?.is_staff ? location : LocationHead.USER;
|
||||
}
|
||||
if (schema.owner === user?.id) {
|
||||
return location;
|
||||
}
|
||||
return head === LocationHead.USER ? LocationHead.USER : location;
|
||||
}
|
||||
|
||||
function handleCreateCst(data: ICstCreateDTO) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
data.alias = data.alias || generateAlias(data.cst_type, schema);
|
||||
cstCreate({ itemID: itemID, data }, newCst => {
|
||||
toast.success(information.newConstituent(newCst.alias));
|
||||
setSelected([newCst.id]);
|
||||
if (onCreateCst) onCreateCst(newCst);
|
||||
navigateRSForm({ tab: activeTab, activeID: newCst.id });
|
||||
if (activeTab === RSTabID.CST_LIST) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end'
|
||||
});
|
||||
}
|
||||
|
||||
function handleRenameCst(data: ICstRenameDTO) {
|
||||
const oldAlias = renameInitialData?.alias ?? '';
|
||||
cstRename({ itemID: itemID, data }, () => toast.success(information.renameComplete(oldAlias, data.alias)));
|
||||
}, PARAMETER.refreshTimeout);
|
||||
}
|
||||
|
||||
function handleSubstituteCst(data: ICstSubstitutions) {
|
||||
cstSubstitute({ itemID: itemID, data }, () => {
|
||||
setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)));
|
||||
toast.success(information.substituteSingle);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteCst(deleted: ConstituentaID[]) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
items: deleted
|
||||
};
|
||||
|
@ -304,88 +210,18 @@ export const RSEditState = ({
|
|||
cstDelete({ itemID: itemID, data }, () => {
|
||||
toast.success(information.constituentsDestroyed(deletedNames));
|
||||
setSelected(nextActive ? [nextActive] : []);
|
||||
onDeleteCst?.(nextActive);
|
||||
});
|
||||
if (!nextActive) {
|
||||
navigateRSForm({ tab: RSTabID.CST_LIST });
|
||||
} else if (activeTab === RSTabID.CST_EDIT) {
|
||||
navigateRSForm({ tab: activeTab, activeID: nextActive });
|
||||
} else {
|
||||
navigateRSForm({ tab: activeTab });
|
||||
}
|
||||
|
||||
function handleSaveWordforms(forms: TermForm[]) {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
const data: ICstUpdateDTO = {
|
||||
target: activeCst.id,
|
||||
item_data: { term_forms: forms }
|
||||
};
|
||||
cstUpdate({ itemID: itemID, data }, () => toast.success(information.changesSaved));
|
||||
}
|
||||
|
||||
function handleCreateVersion(data: IVersionData) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
model.versionCreate(data, () => {
|
||||
toast.success(information.newVersion(data.version));
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteVersion(versionID: VersionID) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
model.versionDelete(versionID, () => {
|
||||
toast.success(information.versionDestroyed);
|
||||
if (versionID === versionID) {
|
||||
viewVersion(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleUpdateVersion(versionID: VersionID, data: IVersionData) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
model.versionUpdate(versionID, data, () => toast.success(information.changesSaved));
|
||||
}
|
||||
|
||||
const handleSetLocation = (newLocation: string) =>
|
||||
setItemLocation({ itemID: itemID, location: newLocation }, () => toast.success(information.moveComplete));
|
||||
|
||||
function handleInlineSynthesis(data: IInlineSynthesisDTO) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
const oldCount = schema.items.length;
|
||||
inlineSynthesisInternal({ itemID: itemID, data }, newSchema => {
|
||||
setSelected([]);
|
||||
toast.success(information.addedConstituents(newSchema.items.length - oldCount));
|
||||
});
|
||||
}
|
||||
|
||||
function createVersion() {
|
||||
if (!schema || (isModified && !promptUnsaved())) {
|
||||
return;
|
||||
}
|
||||
showCreateVersion({
|
||||
versions: schema.versions,
|
||||
onCreate: handleCreateVersion,
|
||||
selected: selected,
|
||||
totalCount: schema.items.length
|
||||
});
|
||||
}
|
||||
|
||||
function promptEditVersions() {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
showEditVersions({
|
||||
versions: schema.versions,
|
||||
onDelete: handleDeleteVersion,
|
||||
onUpdate: handleUpdateVersion
|
||||
});
|
||||
}
|
||||
|
||||
function moveUp() {
|
||||
if (!schema?.items || selected.length === 0) {
|
||||
if (!schema.items || selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
|
@ -407,7 +243,7 @@ export const RSEditState = ({
|
|||
}
|
||||
|
||||
function moveDown() {
|
||||
if (!schema?.items || selected.length === 0) {
|
||||
if (!schema.items || selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
|
@ -433,9 +269,6 @@ export const RSEditState = ({
|
|||
}
|
||||
|
||||
function createCst(type: CstType | undefined, skipDialog: boolean, definition?: string) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
|
||||
const data: ICstCreateDTO = {
|
||||
insert_after: activeCst?.id ?? null,
|
||||
|
@ -455,7 +288,7 @@ export const RSEditState = ({
|
|||
}
|
||||
|
||||
function cloneCst() {
|
||||
if (!activeCst || !schema) {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
const data: ICstCreateDTO = {
|
||||
|
@ -471,213 +304,50 @@ export const RSEditState = ({
|
|||
handleCreateCst(data);
|
||||
}
|
||||
|
||||
function renameCst() {
|
||||
if (!activeCst || !schema) {
|
||||
return;
|
||||
}
|
||||
const data: ICstRenameDTO = {
|
||||
target: activeCst.id,
|
||||
alias: activeCst.alias,
|
||||
cst_type: activeCst.cst_type
|
||||
};
|
||||
setRenameInitialData(data);
|
||||
showRenameCst({
|
||||
schema: schema,
|
||||
initial: data,
|
||||
allowChangeType: !activeCst.is_inherited,
|
||||
onRename: handleRenameCst
|
||||
});
|
||||
}
|
||||
|
||||
function substitute() {
|
||||
if (!schema || (isModified && !promptUnsaved())) {
|
||||
return;
|
||||
}
|
||||
showSubstituteCst({ schema: schema, onSubstitute: handleSubstituteCst });
|
||||
}
|
||||
|
||||
function inlineSynthesis() {
|
||||
if (!schema || (isModified && !promptUnsaved())) {
|
||||
return;
|
||||
}
|
||||
showInlineSynthesis({ receiver: schema, onInlineSynthesis: handleInlineSynthesis });
|
||||
}
|
||||
|
||||
function promptDeleteCst() {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
showDeleteCst({ schema: schema, selected: selected, onDelete: handleDeleteCst });
|
||||
}
|
||||
|
||||
function editTermForms() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
showEditTerm({ target: activeCst, onSave: handleSaveWordforms });
|
||||
}
|
||||
|
||||
function reindex() {
|
||||
if (!itemID) {
|
||||
return;
|
||||
}
|
||||
resetAliasesInternal(itemID, () => toast.success(information.reindexComplete));
|
||||
}
|
||||
|
||||
function reorder() {
|
||||
if (!itemID) {
|
||||
return;
|
||||
}
|
||||
restoreOrderInternal(itemID, () => toast.success(information.reorderComplete));
|
||||
}
|
||||
|
||||
function produceStructure() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
produceStructureInternal({ itemID: itemID, data: { target: activeCst.id } }, cstList => {
|
||||
toast.success(information.addedConstituents(cstList.length));
|
||||
if (cstList.length !== 0) {
|
||||
setSelected(cstList);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleSetEditors(newEditors: UserID[]) {
|
||||
setItemEditors({ itemID: itemID, editors: newEditors }, () => toast.success(information.changesSaved));
|
||||
}
|
||||
|
||||
function promptTemplate() {
|
||||
if ((isModified && !promptUnsaved()) || !schema) {
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
showCstTemplate({ schema: schema, onCreate: handleCreateCst, insertAfter: activeCst?.id });
|
||||
}
|
||||
|
||||
function promptClone() {
|
||||
if (!schema || (isModified && !promptUnsaved())) {
|
||||
return;
|
||||
}
|
||||
showClone({
|
||||
base: schema,
|
||||
initialLocation: calculateCloneLocation(),
|
||||
selected: selected,
|
||||
totalCount: schema.items.length
|
||||
});
|
||||
}
|
||||
|
||||
function promptEditors() {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
showEditEditors({ editors: schema.editors, setEditors: handleSetEditors });
|
||||
}
|
||||
|
||||
function promptLocation() {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
showEditLocation({ initial: schema.location, onChangeLocation: handleSetLocation });
|
||||
}
|
||||
|
||||
function download() {
|
||||
if ((isModified && !promptUnsaved()) || !schema) {
|
||||
return;
|
||||
}
|
||||
const fileName = (schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
|
||||
downloadFile({ itemID: itemID, version: versionID }, (data: Blob) => {
|
||||
try {
|
||||
fileDownload(data, fileName);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function share() {
|
||||
const currentRef = window.location.href;
|
||||
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => toast.success(information.linkReady))
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
function setOwner(newOwner: UserID) {
|
||||
setItemOwner({ itemID: itemID, owner: newOwner }, () => toast.success(information.changesSaved));
|
||||
}
|
||||
|
||||
function setAccessPolicy(newPolicy: AccessPolicy) {
|
||||
setItemAccessPolicy({ itemID: itemID, policy: newPolicy }, () => toast.success(information.changesSaved));
|
||||
}
|
||||
|
||||
function generateQR(): string {
|
||||
const currentRef = window.location.href;
|
||||
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
|
||||
}
|
||||
|
||||
return (
|
||||
<RSEditContext
|
||||
value={{
|
||||
schema: schema,
|
||||
schema,
|
||||
selected,
|
||||
activeCst,
|
||||
|
||||
isOwned,
|
||||
isArchive,
|
||||
isMutable,
|
||||
isContentEditable,
|
||||
isProcessing,
|
||||
isAttachedToOSS,
|
||||
canProduceStructure,
|
||||
canDeleteSelected,
|
||||
|
||||
setOwner,
|
||||
setAccessPolicy,
|
||||
promptEditors,
|
||||
promptLocation,
|
||||
navigateRSForm,
|
||||
navigateCst,
|
||||
|
||||
setSelected: setSelected,
|
||||
deleteSchema,
|
||||
navigateOss,
|
||||
|
||||
setSelected,
|
||||
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
||||
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
|
||||
toggleSelect: (target: ConstituentaID) =>
|
||||
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
|
||||
deselectAll: () => setSelected([]),
|
||||
|
||||
viewOSS,
|
||||
viewVersion,
|
||||
viewPredecessor,
|
||||
createVersion,
|
||||
restoreVersion,
|
||||
promptEditVersions,
|
||||
|
||||
moveUp,
|
||||
moveDown,
|
||||
createCst,
|
||||
cloneCst,
|
||||
renameCst,
|
||||
promptDeleteCst,
|
||||
editTermForms,
|
||||
|
||||
promptTemplate,
|
||||
promptClone,
|
||||
promptUpload: () => showUpload({ itemID: model.itemID! }),
|
||||
download,
|
||||
share,
|
||||
|
||||
reindex,
|
||||
reorder,
|
||||
inlineSynthesis,
|
||||
produceStructure,
|
||||
substitute,
|
||||
|
||||
showTypeGraph: () => showTypeGraph({ items: typeInfo }),
|
||||
showQR: () => showQR({ target: generateQR() })
|
||||
promptTemplate
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,21 +1,81 @@
|
|||
'use client';
|
||||
|
||||
import axios from 'axios';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { RSFormState } from '@/context/RSFormContext';
|
||||
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||
import Divider from '@/components/ui/Divider';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
|
||||
import { RSEditState, RSTabID } from './RSEditContext';
|
||||
import RSTabs from './RSTabs';
|
||||
|
||||
function RSFormPage() {
|
||||
const params = useParams();
|
||||
const router = useConceptNavigation();
|
||||
const query = useQueryStrings();
|
||||
const version = query.get('v') ?? undefined;
|
||||
const params = useParams();
|
||||
const itemID = params.id ? Number(params.id) : undefined;
|
||||
const version = query.get('v') ? Number(query.get('v')) : undefined;
|
||||
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
|
||||
|
||||
const { isModified } = useModificationStore();
|
||||
useBlockNavigation(isModified);
|
||||
|
||||
if (!itemID) {
|
||||
router.replace(urls.page404);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<RSFormState itemID={params.id ?? ''} versionID={version}>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={({ error }) => (
|
||||
<ProcessError error={error as ErrorData} isArchive={!!version} itemID={itemID} />
|
||||
)}
|
||||
>
|
||||
<RSEditState itemID={itemID} versionID={version} activeTab={activeTab}>
|
||||
<RSTabs />
|
||||
</RSFormState>
|
||||
</RSEditState>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default RSFormPage;
|
||||
|
||||
// ====== Internals =========
|
||||
function ProcessError({
|
||||
error,
|
||||
isArchive,
|
||||
itemID
|
||||
}: {
|
||||
error: ErrorData;
|
||||
isArchive: boolean;
|
||||
itemID?: LibraryItemID;
|
||||
}): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
if (error.response.status === 404) {
|
||||
return (
|
||||
<div className='flex flex-col items-center p-2 mx-auto'>
|
||||
<p>{`Концептуальная схема с указанным идентификатором ${isArchive ? 'и версией ' : ''}отсутствует`}</p>
|
||||
<div className='flex justify-center'>
|
||||
<TextURL text='Библиотека' href='/library' />
|
||||
{isArchive ? <Divider vertical margins='mx-3' /> : null}
|
||||
{isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${itemID}`} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (error.response.status === 403) {
|
||||
return (
|
||||
<div className='flex flex-col items-center p-2 mx-auto'>
|
||||
<p>Владелец ограничил доступ к данной схеме</p>
|
||||
<TextURL text='Библиотека' href='/library' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
|
|
|
@ -1,79 +1,43 @@
|
|||
'use client';
|
||||
|
||||
import axios from 'axios';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useGlobalOss } from '@/app/GlobalOssContext';
|
||||
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import { urls } from '@/app/urls';
|
||||
import { useDeleteItem } from '@/backend/library/useDeleteItem';
|
||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||
import Divider from '@/components/ui/Divider';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsform';
|
||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
import { information, labelVersion, prompts } from '@/utils/labels';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { labelVersion } from '@/utils/labels';
|
||||
|
||||
import { OssTabID } from '../OssPage/OssTabs';
|
||||
import EditorConstituenta from './EditorConstituenta';
|
||||
import EditorRSForm from './EditorRSFormCard';
|
||||
import EditorRSList from './EditorRSList';
|
||||
import EditorTermGraph from './EditorTermGraph';
|
||||
import MenuRSTabs from './MenuRSTabs';
|
||||
import { RSEditState } from './RSEditContext';
|
||||
|
||||
export enum RSTabID {
|
||||
CARD = 0,
|
||||
CST_LIST = 1,
|
||||
CST_EDIT = 2,
|
||||
TERM_GRAPH = 3
|
||||
}
|
||||
import { RSTabID, useRSEdit } from './RSEditContext';
|
||||
|
||||
function RSTabs() {
|
||||
const params = useParams();
|
||||
const router = useConceptNavigation();
|
||||
const query = useQueryStrings();
|
||||
const router = useConceptNavigation();
|
||||
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
|
||||
const itemID = params.id ? Number(params.id) : undefined;
|
||||
const version = query.get('v') ? Number(query.get('v')) : undefined;
|
||||
const cstQuery = query.get('active');
|
||||
|
||||
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
||||
const { schema, loading, errorLoading, isArchive } = useRSFormControl();
|
||||
const { deleteItem } = useDeleteItem();
|
||||
const oss = useGlobalOss();
|
||||
const { setIsModified } = useModificationStore();
|
||||
const { schema, selected, setSelected, navigateRSForm } = useRSEdit();
|
||||
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
useBlockNavigation(isModified);
|
||||
|
||||
const [selected, setSelected] = useState<ConstituentaID[]>([]);
|
||||
const activeCst: IConstituenta | undefined = (() => {
|
||||
if (!schema || selected.length === 0) {
|
||||
return undefined;
|
||||
} else {
|
||||
return schema.cstByID.get(selected.at(-1)!);
|
||||
}
|
||||
})();
|
||||
useEffect(() => setIsModified(false), [setIsModified]);
|
||||
|
||||
useEffect(() => {
|
||||
if (schema) {
|
||||
const oldTitle = document.title;
|
||||
document.title = schema.title;
|
||||
return () => {
|
||||
document.title = oldTitle;
|
||||
};
|
||||
}
|
||||
}, [schema, schema?.title]);
|
||||
}, [schema?.title]);
|
||||
|
||||
useEffect(() => {
|
||||
hideFooter(activeTab !== RSTabID.CARD);
|
||||
|
@ -89,30 +53,6 @@ function RSTabs() {
|
|||
return () => hideFooter(false);
|
||||
}, [activeTab, cstQuery, setSelected, schema, hideFooter, setIsModified]);
|
||||
|
||||
function navigateTab(tab: RSTabID, activeID?: ConstituentaID) {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
const url = urls.schema_props({
|
||||
id: schema.id,
|
||||
tab: tab,
|
||||
active: activeID,
|
||||
version: version
|
||||
});
|
||||
if (activeID) {
|
||||
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
|
||||
router.replace(url);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
|
||||
activeID = schema.items[0].id;
|
||||
router.replace(url);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectTab(index: number, last: number, event: Event) {
|
||||
if (last === index) {
|
||||
return;
|
||||
|
@ -129,71 +69,11 @@ function RSTabs() {
|
|||
}
|
||||
}
|
||||
}
|
||||
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined);
|
||||
}
|
||||
|
||||
function onCreateCst(newCst: IConstituentaMeta) {
|
||||
navigateTab(activeTab, newCst.id);
|
||||
if (activeTab === RSTabID.CST_LIST) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end'
|
||||
});
|
||||
}
|
||||
}, PARAMETER.refreshTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
function onDeleteCst(newActive?: ConstituentaID) {
|
||||
if (!newActive) {
|
||||
navigateTab(RSTabID.CST_LIST);
|
||||
} else if (activeTab === RSTabID.CST_EDIT) {
|
||||
navigateTab(activeTab, newActive);
|
||||
} else {
|
||||
navigateTab(activeTab);
|
||||
}
|
||||
}
|
||||
|
||||
function onOpenCst(cstID: ConstituentaID) {
|
||||
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
|
||||
navigateTab(RSTabID.CST_EDIT, cstID);
|
||||
}
|
||||
}
|
||||
|
||||
function onDestroySchema() {
|
||||
if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
|
||||
return;
|
||||
}
|
||||
const backToOSS = oss.schema?.schemas.includes(schema.id);
|
||||
deleteItem(schema.id, () => {
|
||||
toast.success(information.itemDestroyed);
|
||||
if (backToOSS) {
|
||||
oss
|
||||
.invalidate()
|
||||
.then(() => router.push(urls.oss(oss.schema!.id, OssTabID.GRAPH)))
|
||||
.catch(console.error);
|
||||
} else {
|
||||
router.push(urls.library);
|
||||
}
|
||||
});
|
||||
navigateRSForm({ tab: index, activeID: selected.length > 0 ? selected.at(-1) : undefined });
|
||||
}
|
||||
|
||||
return (
|
||||
<RSEditState
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
activeCst={activeCst}
|
||||
isModified={isModified}
|
||||
onCreateCst={onCreateCst}
|
||||
onDeleteCst={onDeleteCst}
|
||||
>
|
||||
{loading ? <Loader /> : null}
|
||||
{errorLoading ? <ProcessError error={errorLoading} isArchive={isArchive} itemID={itemID} /> : null}
|
||||
{schema && !loading ? (
|
||||
<>
|
||||
<Tabs
|
||||
selectedIndex={activeTab}
|
||||
onSelect={onSelectTab}
|
||||
|
@ -205,14 +85,12 @@ function RSTabs() {
|
|||
<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} />
|
||||
<MenuRSTabs />
|
||||
|
||||
<TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} />
|
||||
<TabLabel
|
||||
label='Содержание'
|
||||
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${
|
||||
schema.stats?.count_errors ?? 0
|
||||
}`}
|
||||
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${schema.stats?.count_errors ?? 0}`}
|
||||
/>
|
||||
<TabLabel label='Редактор' />
|
||||
<TabLabel label='Граф термов' />
|
||||
|
@ -221,68 +99,24 @@ function RSTabs() {
|
|||
|
||||
<div className='overflow-x-hidden'>
|
||||
<TabPanel>
|
||||
<EditorRSForm
|
||||
isModified={isModified} //
|
||||
setIsModified={setIsModified}
|
||||
onDestroy={onDestroySchema}
|
||||
/>
|
||||
<EditorRSForm />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<EditorRSList onOpenEdit={onOpenCst} />
|
||||
<EditorRSList />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<EditorConstituenta
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
activeCst={activeCst}
|
||||
onOpenEdit={onOpenCst}
|
||||
/>
|
||||
<EditorConstituenta />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<EditorTermGraph onOpenEdit={onOpenCst} />
|
||||
<EditorTermGraph />
|
||||
</TabPanel>
|
||||
</div>
|
||||
</Tabs>
|
||||
) : null}
|
||||
</RSEditState>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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} />;
|
||||
}
|
||||
|
|
|
@ -4,12 +4,13 @@ import clsx from 'clsx';
|
|||
import { useState } from 'react';
|
||||
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
import { UserRole } from '@/models/user';
|
||||
import { useFitHeight } from '@/stores/appLayout';
|
||||
import { useRoleStore } from '@/stores/role';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import ConstituentsSearch from './ConstituentsSearch';
|
||||
import TableSideConstituents from './TableSideConstituents';
|
||||
|
||||
|
@ -19,16 +20,14 @@ const COLUMN_DENSE_SEARCH_THRESHOLD = 1100;
|
|||
interface ViewConstituentsProps {
|
||||
expression: string;
|
||||
isBottom?: boolean;
|
||||
activeCst?: IConstituenta;
|
||||
schema?: IRSForm;
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
isMounted: boolean;
|
||||
}
|
||||
|
||||
function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit, isMounted }: ViewConstituentsProps) {
|
||||
function ViewConstituents({ expression, isBottom, isMounted }: ViewConstituentsProps) {
|
||||
const windowSize = useWindowSize();
|
||||
const role = useRoleStore(state => state.role);
|
||||
const listHeight = useFitHeight(!isBottom ? '8.2rem' : role !== UserRole.READER ? '42rem' : '35rem', '10rem');
|
||||
const { schema, activeCst, navigateCst } = useRSEdit();
|
||||
|
||||
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
|
||||
|
||||
|
@ -60,7 +59,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
|
|||
maxHeight={listHeight}
|
||||
items={filteredData}
|
||||
activeCst={activeCst}
|
||||
onOpenEdit={onOpenEdit}
|
||||
onOpenEdit={navigateCst}
|
||||
autoScroll={!isBottom}
|
||||
/>
|
||||
</div>
|
||||
|
|
11
rsconcept/frontend/src/stores/modification.ts
Normal file
11
rsconcept/frontend/src/stores/modification.ts
Normal 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 })
|
||||
}));
|
|
@ -3,10 +3,11 @@
|
|||
*/
|
||||
|
||||
import axios, { AxiosError, AxiosHeaderValue, AxiosResponse } from 'axios';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { AliasMapping } from '@/models/rslang';
|
||||
|
||||
import { prompts } from './labels';
|
||||
import { information, prompts } from './labels';
|
||||
|
||||
/**
|
||||
* Checks if arguments is Node.
|
||||
|
@ -202,3 +203,23 @@ export function convertToCSV(targetObj: object[]): Blob {
|
|||
|
||||
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a QR code for the current page.
|
||||
*/
|
||||
export function generatePageQR(): string {
|
||||
const currentRef = window.location.href;
|
||||
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies sharable link to the current page.
|
||||
*/
|
||||
export function sharePage() {
|
||||
const currentRef = window.location.href;
|
||||
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => toast.success(information.linkReady))
|
||||
.catch(console.error);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user