Compare commits

...

7 Commits

Author SHA1 Message Date
Ivan
7f06777237 B: Fix constituenta tooltip
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
2024-09-18 15:09:17 +03:00
Ivan
17ec9ea69d M: Edit manuals 2024-09-18 12:54:16 +03:00
Ivan
93f2e772f6 M: allow single operand synthesis 2024-09-16 20:01:48 +03:00
Ivan
a919cf6baa M: Minor UI improvements 2024-09-16 14:06:42 +03:00
Ivan
3e707eb933 M: Add modifications check for operation editor 2024-09-16 13:45:58 +03:00
Ivan
0a0342efc6 M: Improve Library location navigation 2024-09-15 21:18:32 +03:00
Ivan
b3ecae5d3d M: Improve InlineSynthesis UI 2024-09-15 13:20:25 +03:00
21 changed files with 223 additions and 119 deletions

View File

@ -216,6 +216,7 @@
"Терминологизация", "Терминологизация",
"троллинг", "троллинг",
"Тулисов", "Тулисов",
"Хаданович",
"Цермелло", "Цермелло",
"ЦИВТ", "ЦИВТ",
"Чувашов", "Чувашов",

View File

@ -16,7 +16,7 @@ interface BadgeConstituentaProps extends CProps.Styling {
function BadgeConstituenta({ value, prefixID, className, style, theme }: BadgeConstituentaProps) { function BadgeConstituenta({ value, prefixID, className, style, theme }: BadgeConstituentaProps) {
return ( return (
<div <div
id={`${prefixID}${value.alias}`} id={`${prefixID}${value.id}`}
className={clsx( className={clsx(
'min-w-[3.1rem] max-w-[3.1rem]', 'min-w-[3.1rem] max-w-[3.1rem]',
'px-1', 'px-1',
@ -33,7 +33,7 @@ function BadgeConstituenta({ value, prefixID, className, style, theme }: BadgeCo
}} }}
> >
{value.alias} {value.alias}
<TooltipConstituenta anchor={`#${prefixID}${value.alias}`} data={value} /> <TooltipConstituenta anchor={`#${prefixID}${value.id}`} data={value} />
</div> </div>
); );
} }

View File

@ -182,7 +182,11 @@ function PickSubstitutions({
id: 'left_alias', id: 'left_alias',
size: 65, size: 65,
cell: props => ( cell: props => (
<BadgeConstituenta theme={colors} value={props.row.original.substitution} prefixID={`${prefixID}_1_`} /> <BadgeConstituenta
theme={colors}
value={props.row.original.substitution}
prefixID={`${prefixID}_${props.row.index}_1_`}
/>
) )
}), }),
columnHelper.display({ columnHelper.display({
@ -194,7 +198,11 @@ function PickSubstitutions({
id: 'right_alias', id: 'right_alias',
size: 65, size: 65,
cell: props => ( cell: props => (
<BadgeConstituenta theme={colors} value={props.row.original.original} prefixID={`${prefixID}_2_`} /> <BadgeConstituenta
theme={colors}
value={props.row.original.original}
prefixID={`${prefixID}_${props.row.index}_2_`}
/>
) )
}), }),
columnHelper.accessor(item => item.original_source.alias, { columnHelper.accessor(item => item.original_source.alias, {

View File

@ -34,6 +34,12 @@ interface IOptionsContext {
showHelp: boolean; showHelp: boolean;
toggleShowHelp: () => void; toggleShowHelp: () => void;
folderMode: boolean;
setFolderMode: React.Dispatch<React.SetStateAction<boolean>>;
location: string;
setLocation: React.Dispatch<React.SetStateAction<string>>;
calculateHeight: (offset: string, minimum?: string) => string; calculateHeight: (offset: string, minimum?: string) => string;
} }
@ -56,6 +62,9 @@ export const OptionsState = ({ children }: OptionsStateProps) => {
const [showHelp, setShowHelp] = useLocalStorage(storage.optionsHelp, true); const [showHelp, setShowHelp] = useLocalStorage(storage.optionsHelp, true);
const [noNavigation, setNoNavigation] = useState(false); const [noNavigation, setNoNavigation] = useState(false);
const [folderMode, setFolderMode] = useLocalStorage<boolean>(storage.librarySearchFolderMode, true);
const [location, setLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
const [colors, setColors] = useState<IColorTheme>(lightT); const [colors, setColors] = useState<IColorTheme>(lightT);
const [noNavigationAnimation, setNoNavigationAnimation] = useState(false); const [noNavigationAnimation, setNoNavigationAnimation] = useState(false);
@ -131,6 +140,10 @@ export const OptionsState = ({ children }: OptionsStateProps) => {
noNavigationAnimation, noNavigationAnimation,
noNavigation, noNavigation,
noFooter, noFooter,
folderMode,
setFolderMode,
location,
setLocation,
showScroll, showScroll,
showHelp, showHelp,
toggleDarkMode: toggleDarkMode, toggleDarkMode: toggleDarkMode,

View File

@ -301,12 +301,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
oss.setData(newData); oss.setData(newData);
library.localUpdateTimestamp(newData.id); library.reloadItems(() => {
if (callback) callback(); if (callback) callback();
});
} }
}); });
}, },
[itemID, model, library.localUpdateTimestamp, oss.setData] [itemID, model, library.reloadItems, oss.setData]
); );
const updateOperation = useCallback( const updateOperation = useCallback(

View File

@ -39,7 +39,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText='Создать' submitText='Создать'
className='cc-column w-[35rem] h-[30rem] py-2 px-6' className='cc-column w-[35rem] max-h-[30rem] py-2 px-6'
> >
<FormCreateCst schema={schema} state={cstData} partialUpdate={updateCstData} setValidated={setValidated} /> <FormCreateCst schema={schema} state={cstData} partialUpdate={updateCstData} setValidated={setValidated} />
</Modal> </Modal>

View File

@ -45,7 +45,7 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
if (alias === '') { if (alias === '') {
return false; return false;
} }
if (activeTab === TabID.SYNTHESIS && inputs.length === 1) { if (activeTab === TabID.SYNTHESIS && inputs.length === 0) {
return false; return false;
} }
if (activeTab === TabID.INPUT && !attachedID) { if (activeTab === TabID.INPUT && !attachedID) {

View File

@ -48,7 +48,8 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
const [isCorrect, setIsCorrect] = useState(true); const [isCorrect, setIsCorrect] = useState(true);
const [validationText, setValidationText] = useState(''); const [validationText, setValidationText] = useState('');
const [inputs, setInputs] = useState<OperationID[]>(oss.graph.expandInputs([target.id])); const initialInputs = useMemo(() => oss.graph.expandInputs([target.id]), [oss.graph, target.id]);
const [inputs, setInputs] = useState<OperationID[]>(initialInputs);
const inputOperations = useMemo(() => inputs.map(id => oss.operationByID.get(id)!), [inputs, oss.operationByID]); const inputOperations = useMemo(() => inputs.map(id => oss.operationByID.get(id)!), [inputs, oss.operationByID]);
const schemasIDs = useMemo( const schemasIDs = useMemo(
() => inputOperations.map(operation => operation.result).filter(id => id !== null), () => inputOperations.map(operation => operation.result).filter(id => id !== null),
@ -64,7 +65,28 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
[schemasIDs, cache.getSchema] [schemasIDs, cache.getSchema]
); );
const canSubmit = useMemo(() => alias !== '', [alias]); const isModified = useMemo(
() =>
alias !== target.alias ||
title !== target.title ||
comment !== target.comment ||
JSON.stringify(initialInputs) !== JSON.stringify(inputs) ||
JSON.stringify(substitutions) !== JSON.stringify(target.substitutions),
[
alias,
title,
comment,
target.alias,
target.title,
target.comment,
initialInputs,
inputs,
substitutions,
target.substitutions
]
);
const canSubmit = useMemo(() => isModified && alias !== '', [isModified, alias]);
useLayoutEffect(() => { useLayoutEffect(() => {
cache.preload(schemasIDs); cache.preload(schemasIDs);

View File

@ -23,7 +23,15 @@ function TabSchema({ selected, receiver, setSelected }: TabSchemaProps) {
return ( return (
<AnimateFade className='flex flex-col'> <AnimateFade className='flex flex-col'>
<div className='flex items-center gap-6'> <PickSchema
id='dlg_schema_picker' // prettier: split lines
items={sortedItems}
itemType={LibraryItemType.RSFORM}
rows={14}
value={selected}
onSelectValue={setSelected}
/>
<div className='flex items-center gap-6 '>
<span className='select-none'>Выбрана</span> <span className='select-none'>Выбрана</span>
<TextInput <TextInput
id='dlg_selected_schema_title' id='dlg_selected_schema_title'
@ -35,14 +43,6 @@ function TabSchema({ selected, receiver, setSelected }: TabSchemaProps) {
dense dense
/> />
</div> </div>
<PickSchema
id='dlg_schema_picker' // prettier: split lines
items={sortedItems}
itemType={LibraryItemType.RSFORM}
rows={14}
value={selected}
onSelectValue={setSelected}
/>
</AnimateFade> </AnimateFade>
); );
} }

View File

@ -20,17 +20,18 @@ import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import useLocalStorage from '@/hooks/useLocalStorage';
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library'; import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
import { ILibraryCreateData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library';
import { combineLocation, validateLocation } from '@/models/libraryAPI'; import { combineLocation, validateLocation } from '@/models/libraryAPI';
import { EXTEOR_TRS_FILE, storage } from '@/utils/constants'; import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
function FormCreateItem() { function FormCreateItem() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const options = useConceptOptions();
const { user } = useAuth(); const { user } = useAuth();
const { createItem, processingError, setProcessingError, processing, folders } = useLibrary(); const { createItem, processingError, setProcessingError, processing, folders } = useLibrary();
@ -46,7 +47,6 @@ function FormCreateItem() {
const location = useMemo(() => combineLocation(head, body), [head, body]); const location = useMemo(() => combineLocation(head, body), [head, body]);
const isValid = useMemo(() => validateLocation(location), [location]); const isValid = useMemo(() => validateLocation(location), [location]);
const [initLocation, setInitLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
const [fileName, setFileName] = useState(''); const [fileName, setFileName] = useState('');
const [file, setFile] = useState<File | undefined>(); const [file, setFile] = useState<File | undefined>();
@ -81,7 +81,7 @@ function FormCreateItem() {
file: file, file: file,
fileName: file?.name fileName: file?.name
}; };
setInitLocation(location); options.setLocation(location);
createItem(data, newItem => { createItem(data, newItem => {
toast.success(information.newLibraryItem); toast.success(information.newLibraryItem);
if (itemType == LibraryItemType.RSFORM) { if (itemType == LibraryItemType.RSFORM) {
@ -108,11 +108,11 @@ function FormCreateItem() {
}, []); }, []);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!initLocation) { if (!options.location) {
return; return;
} }
handleSelectLocation(initLocation); handleSelectLocation(options.location);
}, [initLocation, handleSelectLocation]); }, [options.location, handleSelectLocation]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (itemType !== LibraryItemType.RSFORM) { if (itemType !== LibraryItemType.RSFORM) {

View File

@ -10,6 +10,7 @@ import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import DataLoader from '@/components/wrap/DataLoader'; import DataLoader from '@/components/wrap/DataLoader';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
@ -27,14 +28,13 @@ function LibraryPage() {
const library = useLibrary(); const library = useLibrary();
const { user } = useAuth(); const { user } = useAuth();
const [items, setItems] = useState<ILibraryItem[]>([]); const [items, setItems] = useState<ILibraryItem[]>([]);
const options = useConceptOptions();
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [path, setPath] = useState(''); const [path, setPath] = useState('');
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined); const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
const [folderMode, setFolderMode] = useLocalStorage<boolean>(storage.librarySearchFolderMode, true);
const [subfolders, setSubfolders] = useLocalStorage<boolean>(storage.librarySearchSubfolders, false); const [subfolders, setSubfolders] = useLocalStorage<boolean>(storage.librarySearchSubfolders, false);
const [location, setLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true); const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
const [isOwned, setIsOwned] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined); const [isOwned, setIsOwned] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
const [isEditor, setIsEditor] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined); const [isEditor, setIsEditor] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
@ -48,11 +48,11 @@ function LibraryPage() {
isEditor: user ? isEditor : undefined, isEditor: user ? isEditor : undefined,
isOwned: user ? isOwned : undefined, isOwned: user ? isOwned : undefined,
isVisible: user ? isVisible : true, isVisible: user ? isVisible : true,
folderMode: folderMode, folderMode: options.folderMode,
subfolders: subfolders, subfolders: subfolders,
location: location location: options.location
}), }),
[head, path, query, isEditor, isOwned, isVisible, user, folderMode, location, subfolders] [head, path, query, isEditor, isOwned, isVisible, user, options.folderMode, options.location, subfolders]
); );
const hasCustomFilter = useMemo( const hasCustomFilter = useMemo(
@ -74,7 +74,7 @@ function LibraryPage() {
const toggleVisible = useCallback(() => setIsVisible(prev => toggleTristateFlag(prev)), [setIsVisible]); const toggleVisible = useCallback(() => setIsVisible(prev => toggleTristateFlag(prev)), [setIsVisible]);
const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]); const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]);
const toggleEditor = useCallback(() => setIsEditor(prev => toggleTristateFlag(prev)), [setIsEditor]); const toggleEditor = useCallback(() => setIsEditor(prev => toggleTristateFlag(prev)), [setIsEditor]);
const toggleFolderMode = useCallback(() => setFolderMode(prev => !prev), [setFolderMode]); const toggleFolderMode = useCallback(() => options.setFolderMode(prev => !prev), [options.setFolderMode]);
const toggleSubfolders = useCallback(() => setSubfolders(prev => !prev), [setSubfolders]); const toggleSubfolders = useCallback(() => setSubfolders(prev => !prev), [setSubfolders]);
const resetFilter = useCallback(() => { const resetFilter = useCallback(() => {
@ -84,8 +84,8 @@ function LibraryPage() {
setIsVisible(true); setIsVisible(true);
setIsOwned(undefined); setIsOwned(undefined);
setIsEditor(undefined); setIsEditor(undefined);
setLocation(''); options.setLocation('');
}, [setHead, setIsVisible, setIsOwned, setIsEditor, setLocation]); }, [setHead, setIsVisible, setIsOwned, setIsEditor, options.setLocation]);
const promptRenameLocation = useCallback(() => { const promptRenameLocation = useCallback(() => {
setShowRenameLocation(true); setShowRenameLocation(true);
@ -94,11 +94,11 @@ function LibraryPage() {
const handleRenameLocation = useCallback( const handleRenameLocation = useCallback(
(newLocation: string) => { (newLocation: string) => {
const data: IRenameLocationData = { const data: IRenameLocationData = {
target: location, target: options.location,
new_location: newLocation new_location: newLocation
}; };
library.renameLocation(data, () => { library.renameLocation(data, () => {
setLocation(newLocation); options.setLocation(newLocation);
toast.success(information.locationRenamed); toast.success(information.locationRenamed);
}); });
}, },
@ -123,18 +123,18 @@ function LibraryPage() {
<TableLibraryItems <TableLibraryItems
resetQuery={resetFilter} resetQuery={resetFilter}
items={items} items={items}
folderMode={folderMode} folderMode={options.folderMode}
toggleFolderMode={toggleFolderMode} toggleFolderMode={toggleFolderMode}
/> />
), ),
[resetFilter, items, folderMode, toggleFolderMode] [resetFilter, items, options.folderMode, toggleFolderMode]
); );
const viewLocations = useMemo( const viewLocations = useMemo(
() => ( () => (
<ViewSideLocation <ViewSideLocation
active={location} active={options.location}
setActive={setLocation} setActive={options.setLocation}
subfolders={subfolders} subfolders={subfolders}
folderTree={library.folders} folderTree={library.folders}
toggleFolderMode={toggleFolderMode} toggleFolderMode={toggleFolderMode}
@ -142,7 +142,7 @@ function LibraryPage() {
onRenameLocation={promptRenameLocation} onRenameLocation={promptRenameLocation}
/> />
), ),
[location, library.folders, setLocation, toggleFolderMode, subfolders] [options.location, library.folders, options.setLocation, toggleFolderMode, subfolders]
); );
return ( return (
@ -154,12 +154,16 @@ function LibraryPage() {
> >
{showRenameLocation ? ( {showRenameLocation ? (
<DlgChangeLocation <DlgChangeLocation
initial={location} initial={options.location}
onChangeLocation={handleRenameLocation} onChangeLocation={handleRenameLocation}
hideWindow={() => setShowRenameLocation(false)} hideWindow={() => setShowRenameLocation(false)}
/> />
) : null} ) : null}
<Overlay position='top-[0.25rem] right-0' layer='z-tooltip'> <Overlay
position={options.noNavigation ? 'top-[0.25rem] right-[3rem]' : 'top-[0.25rem] right-0'}
layer='z-tooltip'
className='transition-all'
>
<MiniButton <MiniButton
title='Выгрузить в формате CSV' title='Выгрузить в формате CSV'
icon={<IconCSV size='1.25rem' className='icon-green' />} icon={<IconCSV size='1.25rem' className='icon-green' />}
@ -183,12 +187,12 @@ function LibraryPage() {
isEditor={isEditor} isEditor={isEditor}
toggleEditor={toggleEditor} toggleEditor={toggleEditor}
resetFilter={resetFilter} resetFilter={resetFilter}
folderMode={folderMode} folderMode={options.folderMode}
toggleFolderMode={toggleFolderMode} toggleFolderMode={toggleFolderMode}
/> />
<div className='flex'> <div className='flex'>
<AnimatePresence initial={false}>{folderMode ? viewLocations : null}</AnimatePresence> <AnimatePresence initial={false}>{options.folderMode ? viewLocations : null}</AnimatePresence>
{viewLibrary} {viewLibrary}
</div> </div>
</DataLoader> </DataLoader>

View File

@ -96,15 +96,20 @@ function ToolbarSearch({
<div <div
className={clsx( className={clsx(
'sticky top-0', // prettier: split lines 'sticky top-0', // prettier: split lines
'w-full h-[2.2rem]', 'h-[2.2rem]',
'flex items-center', 'flex items-center gap-3',
'border-b', 'border-b',
'text-sm', 'text-sm',
'clr-input' 'clr-input'
)} )}
> >
<div <div
className={clsx('px-3 pt-1 self-center', 'min-w-[6rem] sm:min-w-[7rem]', 'select-none', 'whitespace-nowrap')} className={clsx(
'ml-3 pt-1 self-center',
'min-w-[4.5rem] sm:min-w-[5.5rem]',
'select-none',
'whitespace-nowrap'
)}
> >
{filtered} из {total} {filtered} из {total}
</div> </div>
@ -138,7 +143,7 @@ function ToolbarSearch({
</div> </div>
) : null} ) : null}
<div className='flex items-center h-full mx-auto'> <div className='flex h-full'>
<SearchBar <SearchBar
id='library_search' id='library_search'
placeholder='Поиск' placeholder='Поиск'

View File

@ -15,8 +15,8 @@ function HelpMain() {
</p> </p>
<p> <p>
Такие системы называются <LinkTopic text='Концептуальными схемами' topic={HelpTopic.CC_SYSTEM} /> и состоят из Такие системы называются <LinkTopic text='Концептуальными схемами' topic={HelpTopic.CC_SYSTEM} /> и состоят из
отдельных <LinkTopic text='Конституент' topic={HelpTopic.CC_CONSTITUENTA} />, обладающих уникальными отдельных <LinkTopic text='Конституент' topic={HelpTopic.CC_CONSTITUENTA} />, которым даны формальные
обозначениями и формальными определениями. Концептуальные схемы могут быть получены в рамках операций синтеза в{' '} определения. Концептуальные схемы могут связываться путем синтеза в{' '}
<LinkTopic text='Операционной схеме синтеза' topic={HelpTopic.CC_OSS} />. <LinkTopic text='Операционной схеме синтеза' topic={HelpTopic.CC_OSS} />.
</p> </p>
@ -51,7 +51,8 @@ function HelpMain() {
<h2>Поддержка</h2> <h2>Поддержка</h2>
<p> <p>
Портал разрабатывается <TextURL text='Центром Концепт' href={external_urls.concept} /> Портал разрабатывается <TextURL text='Центром Концепт' href={external_urls.concept} /> и вобрал в себя{' '}
<LinkTopic text='многолетнюю работу' topic={HelpTopic.INFO_CONTRIB} /> над средствами экспликации.
</p> </p>
<p> <p>
Портал поддерживает актуальные версии браузеров Chrome, Firefox, Safari, включая мобильные устройства. Портал поддерживает актуальные версии браузеров Chrome, Firefox, Safari, включая мобильные устройства.

View File

@ -281,6 +281,11 @@ function HelpInfo() {
<TextURL text='pyconcept' href={external_urls.git_core} />. <TextURL text='pyconcept' href={external_urls.git_core} />.
</i> </i>
</li> </li>
<li>
2024 Борисов И.Р., Хаданович Б.А. Исследование механизмов проведения сквозных изменений в операционной схеме
синтеза. Разработка прототипа веб-интерфейса синтеза концептуальных схем.
<i> Прототип графического интерфейса для синтеза концептуальных схем.</i>
</li>
</div> </div>
</div> </div>
); );

View File

@ -50,7 +50,7 @@ function NodeContextMenu({
} }
const argumentIDs = controller.schema.graph.expandInputs([operation.id]); const argumentIDs = controller.schema.graph.expandInputs([operation.id]);
if (!argumentIDs || argumentIDs.length < 2) { if (!argumentIDs || argumentIDs.length < 1) {
return false; return false;
} }

View File

@ -24,7 +24,7 @@ import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { OssNode } from '@/models/miscellaneous'; import { OssNode } from '@/models/miscellaneous';
import { OperationID, OperationType } from '@/models/oss'; import { OperationID } from '@/models/oss';
import { PARAMETER, storage } from '@/utils/constants'; import { PARAMETER, storage } from '@/utils/constants';
import { errors } from '@/utils/labels'; import { errors } from '@/utils/labels';
@ -132,48 +132,11 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
return; return;
} }
let target = { x: 0, y: 0 };
const positions = getPositions(); const positions = getPositions();
if (positions.length == 0) { const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
} else if (inputs.length <= 1) {
let inputsNodes = positions.filter(pos =>
controller.schema!.items.find(
operation => operation.operation_type === OperationType.INPUT && operation.id === pos.id
)
);
if (inputsNodes.length > 0) {
inputsNodes = positions;
}
const maxX = Math.max(...inputsNodes.map(node => node.position_x));
const minY = Math.min(...inputsNodes.map(node => node.position_y));
target.x = maxX + 180;
target.y = minY;
} else {
const inputsNodes = positions.filter(pos => inputs.includes(pos.id));
const maxY = Math.max(...inputsNodes.map(node => node.position_y));
const minX = Math.min(...inputsNodes.map(node => node.position_x));
const maxX = Math.max(...inputsNodes.map(node => node.position_x));
target.x = Math.ceil((maxX + minX) / 2 / PARAMETER.ossGridSize) * PARAMETER.ossGridSize;
target.y = maxY + 100;
}
let flagIntersect = false;
do {
flagIntersect = positions.some(
position =>
Math.abs(position.position_x - target.x) < PARAMETER.ossMinDistance &&
Math.abs(position.position_y - target.y) < PARAMETER.ossMinDistance
);
if (flagIntersect) {
target.x += PARAMETER.ossMinDistance;
target.y += PARAMETER.ossMinDistance;
}
} while (flagIntersect);
controller.promptCreateOperation({ controller.promptCreateOperation({
x: target.x, defaultX: target.x,
y: target.y, defaultY: target.y,
inputs: inputs, inputs: inputs,
positions: positions, positions: positions,
callback: () => flow.fitView({ duration: PARAMETER.zoomDuration }) callback: () => flow.fitView({ duration: PARAMETER.zoomDuration })

View File

@ -76,7 +76,7 @@ function ToolbarOssGraph({
} }
const argumentIDs = controller.schema.graph.expandInputs([selectedOperation.id]); const argumentIDs = controller.schema.graph.expandInputs([selectedOperation.id]);
if (!argumentIDs || argumentIDs.length < 2) { if (!argumentIDs || argumentIDs.length < 1) {
return false; return false;
} }

View File

@ -15,9 +15,9 @@ import {
IconShare IconShare
} from '@/components/Icons'; } from '@/components/Icons';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import DropdownDivider from '@/components/ui/DropdownDivider';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import DropdownDivider from '@/components/ui/DropdownDivider';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';

View File

@ -36,8 +36,8 @@ import { errors, information } from '@/utils/labels';
import { RSTabID } from '../RSFormPage/RSTabs'; import { RSTabID } from '../RSFormPage/RSTabs';
export interface ICreateOperationPrompt { export interface ICreateOperationPrompt {
x: number; defaultX: number;
y: number; defaultY: number;
inputs: OperationID[]; inputs: OperationID[];
positions: IOperationPosition[]; positions: IOperationPosition[];
callback: (newID: OperationID) => void; callback: (newID: OperationID) => void;
@ -221,19 +221,58 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[model] [model]
); );
const promptCreateOperation = useCallback(({ x, y, inputs, positions, callback }: ICreateOperationPrompt) => { const promptCreateOperation = useCallback(
setInsertPosition({ x: x, y: y }); ({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) => {
setInitialInputs(inputs); setInsertPosition({ x: defaultX, y: defaultY });
setPositions(positions); setInitialInputs(inputs);
setCreateCallback(() => callback); setPositions(positions);
setShowCreateOperation(true); setCreateCallback(() => callback);
}, []); setShowCreateOperation(true);
},
[]
);
const handleCreateOperation = useCallback( const handleCreateOperation = useCallback(
(data: IOperationCreateData) => { (data: IOperationCreateData) => {
const target = insertPosition;
if (data.item_data.operation_type === OperationType.INPUT) {
let inputsNodes = positions.filter(pos =>
model.schema!.items.find(
operation => operation.operation_type === OperationType.INPUT && operation.id === pos.id
)
);
if (inputsNodes.length > 0) {
inputsNodes = positions;
}
const maxX = Math.max(...inputsNodes.map(node => node.position_x));
const minY = Math.min(...inputsNodes.map(node => node.position_y));
target.x = maxX + PARAMETER.ossDistanceX;
target.y = minY;
} else {
const argNodes = positions.filter(pos => data.arguments!.includes(pos.id));
const maxY = Math.max(...argNodes.map(node => node.position_y));
const minX = Math.min(...argNodes.map(node => node.position_x));
const maxX = Math.max(...argNodes.map(node => node.position_x));
target.x = Math.ceil((maxX + minX) / 2 / PARAMETER.ossGridSize) * PARAMETER.ossGridSize;
target.y = maxY + PARAMETER.ossDistanceY;
}
let flagIntersect = false;
do {
flagIntersect = positions.some(
position =>
Math.abs(position.position_x - target.x) < PARAMETER.ossMinDistance &&
Math.abs(position.position_y - target.y) < PARAMETER.ossMinDistance
);
if (flagIntersect) {
target.x += PARAMETER.ossMinDistance;
target.y += PARAMETER.ossMinDistance;
}
} while (flagIntersect);
data.positions = positions; data.positions = positions;
data.item_data.position_x = insertPosition.x; data.item_data.position_x = target.x;
data.item_data.position_y = insertPosition.y; data.item_data.position_y = target.y;
model.createOperation(data, operation => { model.createOperation(data, operation => {
toast.success(information.newOperation(operation.alias)); toast.success(information.newOperation(operation.alias));
if (createCallback) { if (createCallback) {

View File

@ -1,13 +1,25 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { IconDateCreate, IconDateUpdate, IconEditor, IconFolder, IconOwner } from '@/components/Icons'; import { urls } from '@/app/urls';
import {
IconDateCreate,
IconDateUpdate,
IconEditor,
IconFolderEdit,
IconFolderOpened,
IconOwner
} from '@/components/Icons';
import InfoUsers from '@/components/info/InfoUsers'; import InfoUsers from '@/components/info/InfoUsers';
import { CProps } from '@/components/props';
import SelectUser from '@/components/select/SelectUser'; import SelectUser from '@/components/select/SelectUser';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import ValueIcon from '@/components/ui/ValueIcon'; import ValueIcon from '@/components/ui/ValueIcon';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useUsers } from '@/context/UsersContext'; import { useUsers } from '@/context/UsersContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library'; import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
@ -25,6 +37,8 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
const { getUserLabel, users } = useUsers(); const { getUserLabel, users } = useUsers();
const { accessLevel } = useAccessMode(); const { accessLevel } = useAccessMode();
const intl = useIntl(); const intl = useIntl();
const router = useConceptNavigation();
const options = useConceptOptions();
const ownerSelector = useDropdown(); const ownerSelector = useDropdown();
const onSelectUser = useCallback( const onSelectUser = useCallback(
@ -41,20 +55,43 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
[controller, item?.owner, ownerSelector] [controller, item?.owner, ownerSelector]
); );
const handleOpenLibrary = useCallback(
(event: CProps.EventMouse) => {
if (!item) {
return;
}
options.setLocation(item.location);
options.setFolderMode(true);
router.push(urls.library, event.ctrlKey || event.metaKey);
},
[options.setLocation, options.setFolderMode, item, router]
);
if (!item) { if (!item) {
return null; return null;
} }
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
<ValueIcon <div className='flex justify-stretch sm:mb-1 max-w-[30rem] gap-3'>
className='sm:mb-1 text-ellipsis max-w-[30rem]' <MiniButton
icon={<IconFolder size='1.25rem' className='icon-primary' />} noHover
value={item.location} noPadding
title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'} title='Открыть в библиотеке'
onClick={controller.promptLocation} icon={<IconFolderOpened size='1.25rem' className='icon-primary' />}
disabled={isModified || controller.isProcessing || controller.isAttachedToOSS || accessLevel < UserLevel.OWNER} onClick={handleOpenLibrary}
/> />
<ValueIcon
className='text-ellipsis flex-grow'
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
value={item.location}
title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'}
onClick={controller.promptLocation}
disabled={
isModified || controller.isProcessing || controller.isAttachedToOSS || accessLevel < UserLevel.OWNER
}
/>
</div>
{ownerSelector.isOpen ? ( {ownerSelector.isOpen ? (
<Overlay position='top-[-0.5rem] left-[2.5rem] cc-icons'> <Overlay position='top-[-0.5rem] left-[2.5rem] cc-icons'>

View File

@ -27,9 +27,9 @@ import {
IconUpload IconUpload
} from '@/components/Icons'; } from '@/components/Icons';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import DropdownDivider from '@/components/ui/DropdownDivider';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import DropdownDivider from '@/components/ui/DropdownDivider';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useGlobalOss } from '@/context/GlobalOssContext'; import { useGlobalOss } from '@/context/GlobalOssContext';
@ -85,6 +85,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
controller.share(); controller.share();
} }
function handleCreateVersion() {
schemaMenu.hide();
controller.createVersion();
}
function handleReindex() { function handleReindex() {
editMenu.hide(); editMenu.hide();
controller.reindex(); controller.reindex();
@ -161,7 +166,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
<DropdownButton <DropdownButton
text='Сохранить версию' text='Сохранить версию'
disabled={!controller.isContentEditable} disabled={!controller.isContentEditable}
onClick={controller.createVersion} onClick={handleCreateVersion}
icon={<IconNewVersion size='1rem' className='icon-green' />} icon={<IconNewVersion size='1rem' className='icon-green' />}
/> />
<DropdownButton <DropdownButton