Compare commits

...

9 Commits

Author SHA1 Message Date
Ivan
f86d847d64 M: Minor syntax fixes
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
2024-08-23 22:53:32 +03:00
Ivan
b60c1305bd F: Improve stats UI for OSS and RSForm 2024-08-23 21:28:54 +03:00
Ivan
13b7b4d748 F: Improve OSS deletion mechanics 2024-08-23 19:09:18 +03:00
Ivan
eb5cb6945f M: Remove inheritance data from archive schemas 2024-08-23 18:36:17 +03:00
Ivan
1305560679 M: Improve stats labels 2024-08-23 12:35:05 +03:00
Ivan
c864ec947b M: Minor UI fixes 2024-08-22 23:23:26 +03:00
Ivan
7a4fceb9bf F: Implement CSV export 2024-08-22 22:41:29 +03:00
Ivan
e1a95e1d81 M: Improve manuals 2024-08-22 21:38:59 +03:00
Ivan
cdefe97d98 M: Improve RSForm stats UI 2024-08-22 15:33:44 +03:00
40 changed files with 689 additions and 221 deletions

View File

@ -2,33 +2,39 @@
For more specific TODOs see comments in code For more specific TODOs see comments in code
[Functionality - PROGRESS] [Functionality - PROGRESS]
- Design first user experience - OSS change propagation: Advanced features
- Private projects. Consider cooperative editing
[Functionality - PENDING] [Functionality - PENDING]
- Search functionality for manuals
- User notifications on edit - consider spam prevention and change aggregation
- Static analyzer for RSForm as a whole: check term duplication and empty conventions
- Content based search in Library
- User profile: Settings + settings persistency
- Landing page - Landing page
- Home page (user specific) - Design first user experience
- Demo sandbox for anonymous users
- User profile: Settings + settings persistency
- Custom LibraryItem lists
- Custom user filters and sharing filters
- Static analyzer for RSForm as a whole: check term duplication and empty conventions
- OSS clone and versioning
- Focus on codemirror editor when label is clicked (need React 19 ref for clean code solution)
- Draggable rows in constituents table - Draggable rows in constituents table
- replace reagraph with react-flow in TermGraph and FormulaGraph
- Search functionality for Help Manuals
- Export PDF (Items list, Graph) - Export PDF (Items list, Graph)
- ARIA (accessibility considerations) - for now machine reading not supported - ARIA (accessibility considerations) - for now machine reading not supported
- Internationalization - at least english version. Consider react.intl - Internationalization - at least english version. Consider react.intl
- Focus on codemirror editor when label is clicked (need React 19 ref for clean code solution)
- Sitemap for better SEO and crawler optimization - Sitemap for better SEO and crawler optimization
[Functionality - CANCELED]
- User notifications on edit - consider spam prevention and change aggregation
- Content based search in Library
- Home page (user specific)
- Private projects. Consider cooperative editing
[Tech] [Tech]
- duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib - duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib
- add debounce to some search fields - add debounce to some search fields. Consider pagination and dynamic loading
- DataTable: fixed percentage columns, especially for SubstituteTable. Rework column sizing mechanics - DataTable: fixed percentage columns, especially for SubstituteTable. Rework column sizing mechanics
- move autopep8 and isort settings from vscode settings to pyproject.toml - move autopep8 and isort settings from vscode settings to pyproject.toml
- Test UI for #enable-force-dark Chrome setting - Test UI for #enable-force-dark Chrome setting

View File

@ -66,8 +66,14 @@ class LibraryViewSet(viewsets.ModelViewSet):
Operation.objects.bulk_update(update_list, ['alias', 'title', 'comment']) Operation.objects.bulk_update(update_list, ['alias', 'title', 'comment'])
def perform_destroy(self, instance: m.LibraryItem) -> None: def perform_destroy(self, instance: m.LibraryItem) -> None:
PropagationFacade.before_delete_schema(instance) if instance.item_type == m.LibraryItemType.RSFORM:
return super().perform_destroy(instance) PropagationFacade.before_delete_schema(instance)
super().perform_destroy(instance)
if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA:
schemas = list(OperationSchema(instance).owned_schemas())
super().perform_destroy(instance)
for schema in schemas:
self.perform_destroy(schema)
def get_permissions(self): def get_permissions(self):
if self.action in ['update', 'partial_update']: if self.action in ['update', 'partial_update']:

View File

@ -1,5 +1,5 @@
''' Testing API: Change attributes of OSS and RSForms. ''' ''' Testing API: Change attributes of OSS and RSForms. '''
from apps.library.models import AccessPolicy, Editor, LocationHead from apps.library.models import AccessPolicy, Editor, LibraryItem, LocationHead
from apps.oss.models import Operation, OperationSchema, OperationType from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from apps.users.models import User from apps.users.models import User
@ -55,7 +55,7 @@ class TestChangeAttributes(EndpointTester):
) )
self.owned.execute_operation(self.operation3) self.owned.execute_operation(self.operation3)
self.operation3.refresh_from_db() self.operation3.refresh_from_db()
self.ks3 = self.operation3.result self.ks3 = RSForm(self.operation3.result)
@decl_endpoint('/api/library/{item}/set-owner', method='patch') @decl_endpoint('/api/library/{item}/set-owner', method='patch')
@ -71,7 +71,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.owned.model.owner, self.user3) self.assertEqual(self.owned.model.owner, self.user3)
self.assertEqual(self.ks1.model.owner, self.user) self.assertEqual(self.ks1.model.owner, self.user)
self.assertEqual(self.ks2.model.owner, self.user2) self.assertEqual(self.ks2.model.owner, self.user2)
self.assertEqual(self.ks3.owner, self.user3) self.assertEqual(self.ks3.model.owner, self.user3)
@decl_endpoint('/api/library/{item}/set-location', method='patch') @decl_endpoint('/api/library/{item}/set-location', method='patch')
def test_set_location(self): def test_set_location(self):
@ -86,7 +86,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.owned.model.location, data['location']) self.assertEqual(self.owned.model.location, data['location'])
self.assertNotEqual(self.ks1.model.location, data['location']) self.assertNotEqual(self.ks1.model.location, data['location'])
self.assertNotEqual(self.ks2.model.location, data['location']) self.assertNotEqual(self.ks2.model.location, data['location'])
self.assertEqual(self.ks3.location, data['location']) self.assertEqual(self.ks3.model.location, data['location'])
@decl_endpoint('/api/library/{item}/set-access-policy', method='patch') @decl_endpoint('/api/library/{item}/set-access-policy', method='patch')
def test_set_access_policy(self): def test_set_access_policy(self):
@ -101,13 +101,13 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.owned.model.access_policy, data['access_policy']) self.assertEqual(self.owned.model.access_policy, data['access_policy'])
self.assertNotEqual(self.ks1.model.access_policy, data['access_policy']) self.assertNotEqual(self.ks1.model.access_policy, data['access_policy'])
self.assertNotEqual(self.ks2.model.access_policy, data['access_policy']) self.assertNotEqual(self.ks2.model.access_policy, data['access_policy'])
self.assertEqual(self.ks3.access_policy, data['access_policy']) self.assertEqual(self.ks3.model.access_policy, data['access_policy'])
@decl_endpoint('/api/library/{item}/set-editors', method='patch') @decl_endpoint('/api/library/{item}/set-editors', method='patch')
def test_set_editors(self): def test_set_editors(self):
Editor.set(self.owned.model.pk, [self.user2.pk]) Editor.set(self.owned.model.pk, [self.user2.pk])
Editor.set(self.ks1.model.pk, [self.user2.pk, self.user.pk]) Editor.set(self.ks1.model.pk, [self.user2.pk, self.user.pk])
Editor.set(self.ks3.pk, [self.user2.pk, self.user.pk]) Editor.set(self.ks3.model.pk, [self.user2.pk, self.user.pk])
data = {'users': [self.user3.pk]} data = {'users': [self.user3.pk]}
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
@ -119,7 +119,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(list(self.owned.model.editors()), [self.user3]) self.assertEqual(list(self.owned.model.editors()), [self.user3])
self.assertEqual(list(self.ks1.model.editors()), [self.user, self.user2]) self.assertEqual(list(self.ks1.model.editors()), [self.user, self.user2])
self.assertEqual(list(self.ks2.model.editors()), []) self.assertEqual(list(self.ks2.model.editors()), [])
self.assertEqual(set(self.ks3.editors()), set([self.user, self.user3])) self.assertEqual(set(self.ks3.model.editors()), set([self.user, self.user3]))
@decl_endpoint('/api/library/{item}', method='patch') @decl_endpoint('/api/library/{item}', method='patch')
def test_sync_from_result(self): def test_sync_from_result(self):
@ -147,6 +147,13 @@ class TestChangeAttributes(EndpointTester):
response = self.executeOK(data=data, item=self.owned_id) response = self.executeOK(data=data, item=self.owned_id)
self.ks3.refresh_from_db() self.ks3.refresh_from_db()
self.assertEqual(self.ks3.alias, data['item_data']['alias']) self.assertEqual(self.ks3.model.alias, data['item_data']['alias'])
self.assertEqual(self.ks3.title, data['item_data']['title']) self.assertEqual(self.ks3.model.title, data['item_data']['title'])
self.assertEqual(self.ks3.comment, data['item_data']['comment']) self.assertEqual(self.ks3.model.comment, data['item_data']['comment'])
@decl_endpoint('/api/library/{item}', method='delete')
def test_destroy_oss_consequence(self):
response = self.executeNoContent(item=self.owned_id)
self.assertFalse(LibraryItem.objects.filter(pk=self.owned_id).exists())
self.assertFalse(LibraryItem.objects.filter(pk=self.ks3.model.pk).exists())
self.assertTrue(LibraryItem.objects.filter(pk=self.ks1.model.pk).exists())

View File

@ -115,14 +115,19 @@ class RSFormSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
def to_representation(self, instance: LibraryItem) -> dict: def to_representation(self, instance: LibraryItem) -> dict:
result = LibraryItemDetailsSerializer(instance).data result = self.to_base_data(instance)
result['items'] = []
for cst in RSForm(instance).constituents().order_by('order'):
result['items'].append(CstSerializer(cst).data)
result['inheritance'] = []
for link in Inheritance.objects.filter(Q(child__schema=instance) | Q(parent__schema=instance)): for link in Inheritance.objects.filter(Q(child__schema=instance) | Q(parent__schema=instance)):
result['inheritance'].append([link.child.pk, link.parent.pk]) result['inheritance'].append([link.child.pk, link.parent.pk])
return result
def to_base_data(self, instance: LibraryItem) -> dict:
''' Create serializable base representation without redundant data. '''
result = LibraryItemDetailsSerializer(instance).data
result['items'] = []
result['oss'] = [] result['oss'] = []
result['inheritance'] = []
for cst in RSForm(instance).constituents().order_by('order'):
result['items'].append(CstSerializer(cst).data)
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'): for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
result['oss'].append({ result['oss'].append({
'id': oss.pk, 'id': oss.pk,
@ -132,11 +137,9 @@ class RSFormSerializer(serializers.ModelSerializer):
def to_versioned_data(self) -> dict: def to_versioned_data(self) -> dict:
''' Create serializable version representation without redundant data. ''' ''' Create serializable version representation without redundant data. '''
result = self.to_representation(cast(LibraryItem, self.instance)) result = self.to_base_data(cast(LibraryItem, self.instance))
del result['versions'] del result['versions']
del result['editors'] del result['editors']
del result['inheritance']
del result['oss']
del result['owner'] del result['owner']
del result['visible'] del result['visible']
@ -150,7 +153,7 @@ class RSFormSerializer(serializers.ModelSerializer):
def from_versioned_data(self, version: int, data: dict) -> dict: def from_versioned_data(self, version: int, data: dict) -> dict:
''' Load data from version. ''' ''' Load data from version. '''
result = self.to_representation(cast(LibraryItem, self.instance)) result = self.to_base_data(cast(LibraryItem, self.instance))
result['version'] = version result['version'] = version
return result | data return result | data

View File

@ -51,6 +51,7 @@ export { BiFirstPage as IconPageFirst } from 'react-icons/bi';
export { BiLastPage as IconPageLast } from 'react-icons/bi'; export { BiLastPage as IconPageLast } from 'react-icons/bi';
export { TbCalendarPlus as IconDateCreate } from 'react-icons/tb'; export { TbCalendarPlus as IconDateCreate } from 'react-icons/tb';
export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb'; export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
export { PiFileCsv as IconCSV } from 'react-icons/pi';
// ==== User status ======= // ==== User status =======
export { LuUserCircle2 as IconUser } from 'react-icons/lu'; export { LuUserCircle2 as IconUser } from 'react-icons/lu';
@ -68,9 +69,22 @@ export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
export { BiDiamond as IconTemplates } from 'react-icons/bi'; export { BiDiamond as IconTemplates } from 'react-icons/bi';
export { TbHexagons as IconOSS } from 'react-icons/tb'; export { TbHexagons as IconOSS } from 'react-icons/tb';
export { TbHexagon as IconRSForm } from 'react-icons/tb'; export { TbHexagon as IconRSForm } from 'react-icons/tb';
export { TbTopologyRing as IconConsolidation } from 'react-icons/tb'; export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
export { GrInherit as IconChild } from 'react-icons/gr'; export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
export { TbHexagonLetterX as IconCstBaseSet } from 'react-icons/tb';
export { TbHexagonLetterC as IconCstConstSet } from 'react-icons/tb';
export { TbHexagonLetterS as IconCstStructured } from 'react-icons/tb';
export { TbHexagonLetterA as IconCstAxiom } from 'react-icons/tb';
export { TbHexagonLetterD as IconCstTerm } from 'react-icons/tb';
export { TbHexagonLetterF as IconCstFunction } from 'react-icons/tb';
export { TbHexagonLetterP as IconCstPredicate } from 'react-icons/tb';
export { TbHexagonLetterT as IconCstTheorem } from 'react-icons/tb';
export { LuNewspaper as IconDefinition } from 'react-icons/lu';
export { LuDna as IconTerminology } from 'react-icons/lu';
export { FaRegHandshake as IconConvention } from 'react-icons/fa6';
export { LiaCloneSolid as IconChild } from 'react-icons/lia';
export { RiParentLine as IconParent } from 'react-icons/ri'; export { RiParentLine as IconParent } from 'react-icons/ri';
export { TbTopologyRing as IconConsolidation } from 'react-icons/tb';
export { BiSpa as IconPredecessor } from 'react-icons/bi'; export { BiSpa as IconPredecessor } from 'react-icons/bi';
export { LuArchive as IconArchive } from 'react-icons/lu'; export { LuArchive as IconArchive } from 'react-icons/lu';
export { LuDatabase as IconDatabase } from 'react-icons/lu'; export { LuDatabase as IconDatabase } from 'react-icons/lu';
@ -95,7 +109,8 @@ export { RiShieldKeyholeLine as IconPrivate } from 'react-icons/ri';
export { BiBug as IconStatusError } from 'react-icons/bi'; export { BiBug as IconStatusError } from 'react-icons/bi';
export { BiCheckCircle as IconStatusOK } from 'react-icons/bi'; export { BiCheckCircle as IconStatusOK } from 'react-icons/bi';
export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi'; export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
export { BiPauseCircle as IconStatusIncalculable } from 'react-icons/bi'; export { BiStopCircle as IconStatusIncalculable } from 'react-icons/bi';
export { BiPauseCircle as IconStatusProperty } from 'react-icons/bi';
export { LuPower as IconKeepAliasOn } from 'react-icons/lu'; export { LuPower as IconKeepAliasOn } from 'react-icons/lu';
export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu'; export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu';
@ -111,6 +126,7 @@ export { BiDuplicate as IconClone } from 'react-icons/bi';
export { LuReplace as IconReplace } from 'react-icons/lu'; export { LuReplace as IconReplace } from 'react-icons/lu';
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa'; export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu'; export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
export { LuCombine as IconSynthesis } from 'react-icons/lu';
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu'; export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
export { LuWand2 as IconGenerateNames } from 'react-icons/lu'; export { LuWand2 as IconGenerateNames } from 'react-icons/lu';
export { GrConnect as IconConnect } from 'react-icons/gr'; export { GrConnect as IconConnect } from 'react-icons/gr';

View File

@ -151,7 +151,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
<div className={clsx('flex flex-col gap-2', className, cursor)} style={style}> <div className={clsx('flex flex-col gap-2', className, cursor)} style={style}>
<Label text={label} /> <Label text={label} />
<CodeMirror <CodeMirror
className={'font-math'} className='font-math'
id={id} id={id}
ref={thisRef} ref={thisRef}
basicSetup={editorSetup} basicSetup={editorSetup}

View File

@ -1,48 +0,0 @@
import clsx from 'clsx';
import { CProps } from '../props';
import MiniButton from './MiniButton';
interface IconValueProps extends CProps.Styling, CProps.Titled {
id?: string;
icon: React.ReactNode;
value: string | number;
onClick?: (event: CProps.EventMouse) => void;
dense?: boolean;
disabled?: boolean;
}
function IconValue({
id,
dense,
value,
icon,
disabled = true,
title,
titleHtml,
hideTitle,
className,
onClick,
...restProps
}: IconValueProps) {
return (
<div
className={clsx('flex items-center', { 'justify-between gap-6 text-right': !dense, 'gap-2': dense }, className)}
{...restProps}
>
<MiniButton
noHover
noPadding
title={title}
titleHtml={titleHtml}
hideTitle={hideTitle}
icon={icon}
disabled={disabled}
onClick={onClick}
/>
<span id={id}>{value}</span>
</div>
);
}
export default IconValue;

View File

@ -46,7 +46,7 @@ function SelectorButton({
{...restProps} {...restProps}
> >
{icon ? icon : null} {icon ? icon : null}
{text ? <div className={'whitespace-nowrap'}>{text}</div> : null} {text ? <div className='whitespace-nowrap'>{text}</div> : null}
</button> </button>
); );
} }

View File

@ -0,0 +1,59 @@
import clsx from 'clsx';
import { useMemo } from 'react';
import { globals } from '@/utils/constants';
import { CProps } from '../props';
import MiniButton from './MiniButton';
interface ValueIconProps extends CProps.Styling, CProps.Titled {
id?: string;
icon: React.ReactNode;
value: string | number;
textClassName?: string;
onClick?: (event: CProps.EventMouse) => void;
smallThreshold?: number;
dense?: boolean;
disabled?: boolean;
}
function ValueIcon({
id,
dense,
icon,
value,
textClassName,
disabled = true,
title,
titleHtml,
hideTitle,
className,
smallThreshold,
onClick,
...restProps
}: ValueIconProps) {
const isSmall = useMemo(() => !smallThreshold || String(value).length < smallThreshold, [value, smallThreshold]);
return (
<div
className={clsx(
'flex items-center',
'text-right',
'hover:cursor-default',
{ 'justify-between gap-6': !dense, 'gap-1': dense },
className
)}
{...restProps}
data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
>
<MiniButton noHover noPadding icon={icon} disabled={disabled} onClick={onClick} />
<span id={id} className={clsx({ 'text-xs': !isSmall }, textClassName)}>
{value}
</span>
</div>
);
}
export default ValueIcon;

View File

@ -2,14 +2,14 @@ import clsx from 'clsx';
import { CProps } from '../props'; import { CProps } from '../props';
interface LabeledValueProps extends CProps.Styling { interface ValueLabeledProps extends CProps.Styling {
id?: string; id?: string;
label: string; label: string;
text: string | number; text: string | number;
title?: string; title?: string;
} }
function LabeledValue({ id, label, text, title, className, ...restProps }: LabeledValueProps) { function ValueLabeled({ id, label, text, title, className, ...restProps }: ValueLabeledProps) {
return ( return (
<div className={clsx('flex justify-between gap-6', className)} {...restProps}> <div className={clsx('flex justify-between gap-6', className)} {...restProps}>
<span title={title}>{label}</span> <span title={title}>{label}</span>
@ -18,4 +18,4 @@ function LabeledValue({ id, label, text, title, className, ...restProps }: Label
); );
} }
export default LabeledValue; export default ValueLabeled;

View File

@ -0,0 +1,16 @@
import { PARAMETER } from '@/utils/constants';
import { CProps } from '../props';
import ValueIcon from './ValueIcon';
interface ValueStatsProps extends CProps.Styling, CProps.Titled {
id: string;
icon: React.ReactNode;
value: string | number;
}
function ValueStats(props: ValueStatsProps) {
return <ValueIcon dense smallThreshold={PARAMETER.statSmallThreshold} textClassName='min-w-[1.4rem]' {...props} />;
}
export default ValueStats;

View File

@ -65,7 +65,7 @@ function TabInputOperation({
<TextInput <TextInput
id='operation_alias' id='operation_alias'
label='Сокращение' label='Сокращение'
className='w-[14rem]' className='w-[16rem]'
value={alias} value={alias}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
disabled={attachedID !== undefined} disabled={attachedID !== undefined}

View File

@ -42,7 +42,7 @@ function TabSynthesisOperation({
<TextInput <TextInput
id='operation_alias' id='operation_alias'
label='Сокращение' label='Сокращение'
className='w-[14rem]' className='w-[16rem]'
value={alias} value={alias}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />

View File

@ -24,7 +24,7 @@ function TabOperation({ alias, setAlias, title, setTitle, comment, setComment }:
<TextInput <TextInput
id='operation_alias' id='operation_alias'
label='Сокращение' label='Сокращение'
className='w-[14rem]' className='w-[16rem]'
value={alias} value={alias}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />

View File

@ -42,7 +42,7 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
<Modal <Modal
header='Переименование конституенты' header='Переименование конституенты'
submitText='Переименовать' submitText='Переименовать'
submitInvalidTooltip={'Введите незанятое имя, соответствующее типу'} submitInvalidTooltip='Введите незанятое имя, соответствующее типу'
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={validated} canSubmit={validated}
onSubmit={() => onRename(cstData)} onSubmit={() => onRename(cstData)}

View File

@ -30,7 +30,7 @@ function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCst
<Modal <Modal
header='Отождествление' header='Отождествление'
submitText='Отождествить' submitText='Отождествить'
submitInvalidTooltip={'Выберите две различные конституенты'} submitInvalidTooltip='Выберите две различные конституенты'
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={canSubmit} canSubmit={canSubmit}
onSubmit={handleSubmit} onSubmit={handleSubmit}

View File

@ -90,7 +90,8 @@ export class OssLoader {
count_operations: items.length, count_operations: items.length,
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length, count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: items.filter(item => item.operation_type === OperationType.SYNTHESIS).length, count_synthesis: items.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: this.schemaIDs.length count_schemas: this.schemaIDs.length,
count_owned: items.filter(item => !!item.result && item.is_owned).length
}; };
} }
} }

View File

@ -154,6 +154,7 @@ export interface IOperationSchemaStats {
count_inputs: number; count_inputs: number;
count_synthesis: number; count_synthesis: number;
count_schemas: number; count_schemas: number;
count_owned: number;
} }
/** /**

View File

@ -152,7 +152,7 @@ function FormCreateItem() {
required={!file} required={!file}
label='Сокращение' label='Сокращение'
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
className='w-[14rem]' className='w-[16rem]'
value={alias} value={alias}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />

View File

@ -1,9 +1,13 @@
'use client'; 'use client';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import fileDownload from 'js-file-download';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { IconCSV } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton';
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 { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
@ -13,7 +17,7 @@ import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/librar
import { ILibraryFilter } from '@/models/miscellaneous'; import { ILibraryFilter } from '@/models/miscellaneous';
import { storage } from '@/utils/constants'; import { storage } from '@/utils/constants';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
import { toggleTristateFlag } from '@/utils/utils'; import { convertToCSV, toggleTristateFlag } from '@/utils/utils';
import TableLibraryItems from './TableLibraryItems'; import TableLibraryItems from './TableLibraryItems';
import ToolbarSearch from './ToolbarSearch'; import ToolbarSearch from './ToolbarSearch';
@ -101,6 +105,19 @@ function LibraryPage() {
[location, library] [location, library]
); );
const handleDownloadCSV = useCallback(() => {
if (items.length === 0) {
toast.error(information.noDataToExport);
return;
}
const blob = convertToCSV(items);
try {
fileDownload(blob, 'library.csv', 'text/csv;charset=utf-8;');
} catch (error) {
console.error(error);
}
}, [items]);
const viewLibrary = useMemo( const viewLibrary = useMemo(
() => ( () => (
<TableLibraryItems <TableLibraryItems
@ -142,6 +159,13 @@ function LibraryPage() {
hideWindow={() => setShowRenameLocation(false)} hideWindow={() => setShowRenameLocation(false)}
/> />
) : null} ) : null}
<Overlay position='top-[0.25rem] right-0' layer='z-tooltip'>
<MiniButton
title='Выгрузить в формате CSV'
icon={<IconCSV size='1.25rem' className='icon-green' />}
onClick={handleDownloadCSV}
/>
</Overlay>
<ToolbarSearch <ToolbarSearch
total={library.items.length ?? 0} total={library.items.length ?? 0}
filtered={items.length} filtered={items.length}

View File

@ -37,13 +37,14 @@ function HelpMain() {
<h2>Разделы Справки</h2> <h2>Разделы Справки</h2>
{[ {[
HelpTopic.INFO, HelpTopic.THESAURUS,
HelpTopic.INTERFACE, HelpTopic.INTERFACE,
HelpTopic.CONCEPTUAL, HelpTopic.CONCEPTUAL,
HelpTopic.RSLANG, HelpTopic.RSLANG,
HelpTopic.TERM_CONTROL, HelpTopic.TERM_CONTROL,
HelpTopic.ACCESS, HelpTopic.ACCESS,
HelpTopic.VERSIONS, HelpTopic.VERSIONS,
HelpTopic.INFO,
HelpTopic.EXTEOR HelpTopic.EXTEOR
].map(topic => ( ].map(topic => (
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} /> <TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
@ -72,7 +73,7 @@ function HelpMain() {
версию браузера в случае возникновения визуальных ошибок или проблем с производительностью. версию браузера в случае возникновения визуальных ошибок или проблем с производительностью.
</p> </p>
<p> <p>
Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять по email:{' '} Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять на email:{' '}
<TextURL href={external_urls.mail_portal} text='portal@acconcept.ru' /> <TextURL href={external_urls.mail_portal} text='portal@acconcept.ru' />
</p> </p>
</div> </div>

View File

@ -1,4 +1,31 @@
import { IconChild, IconPredecessor, IconRSForm } from '@/components/Icons'; import {
IconChild,
IconConsolidation,
IconCstAxiom,
IconCstBaseSet,
IconCstConstSet,
IconCstFunction,
IconCstPredicate,
IconCstStructured,
IconCstTerm,
IconCstTheorem,
IconDownload,
IconGraphCollapse,
IconGraphExpand,
IconGraphInputs,
IconGraphOutputs,
IconOSS,
IconPredecessor,
IconRSForm,
IconRSFormImported,
IconRSFormOwned,
IconStatusError,
IconStatusIncalculable,
IconStatusOK,
IconStatusProperty,
IconStatusUnknown,
IconSynthesis
} from '@/components/Icons';
import LinkTopic from '@/components/ui/LinkTopic'; import LinkTopic from '@/components/ui/LinkTopic';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
@ -12,11 +39,11 @@ function HelpThesaurus() {
Справки через гиперссылки. Также указываются графические обозначения (иконки, цвета), используемые для Справки через гиперссылки. Также указываются графические обозначения (иконки, цвета), используемые для
обозначения соответствующих сущностей в интерфейсе Портала. обозначения соответствующих сущностей в интерфейсе Портала.
</p> </p>
<h2>Концептуализация</h2>
<p>Раздел в разработке...</p>
<h2>Концептуальная схема</h2> <h2>Концептуальная схема</h2>
<p> <p>
<IconRSForm size='1rem' className='inline-icon' />{' '} <IconRSForm size='1rem' className='inline-icon' />
{'\u2009'}
<LinkTopic text='Концептуальная схема' topic={HelpTopic.CC_SYSTEM} /> (<i>система определений, КС</i>) <LinkTopic text='Концептуальная схема' topic={HelpTopic.CC_SYSTEM} /> (<i>система определений, КС</i>)
совокупность отдельных понятий и утверждений, а также связей между ними, задаваемых определениями. совокупность отдельных понятий и утверждений, а также связей между ними, задаваемых определениями.
</p> </p>
@ -28,6 +55,33 @@ function HelpThesaurus() {
Родоструктурная экспликация КС экспликация КС с помощью{' '} Родоструктурная экспликация КС экспликация КС с помощью{' '}
<LinkTopic text='аппарата родов структур' topic={HelpTopic.RSLANG} />. <LinkTopic text='аппарата родов структур' topic={HelpTopic.RSLANG} />.
</p> </p>
<p>
Граф термов ориентированный граф, узлами которого являются конституенты КС, а связи задаются на основе
вхождения имени конституенты в определение другой конституенты.
</p>
<p>
Ядро концептуальной схемы совокупность базовых понятий, аксиом и промежуточных производных понятий,
необходимых для формирования выражений аксиом. Остальные конституенты относят к Телу концептуальной схемы.
</p>
<ul>
По <b>отношению к операциям ОСС</b> выделены:
<li>
<IconRSForm size='1rem' className='inline-icon' />
{'\u2009'}свободная КС это КС не прикрепленная ни к одной операции в ОСС;
</li>
<li>
<IconRSFormOwned size='1rem' className='inline-icon' />
{'\u2009'}собственная КС данной ОСС это КС, прикрепленная к операции в ОСС, чьи владелец и расположение
совпадают с соответствующими атрибутами ОСС.
</li>
<li>
<IconRSFormImported size='1rem' className='inline-icon' />
{'\u2009'}внешняя КС данной ОСС это КС, прикрепленная к операции в ОСС, чьи владелец или расположение не
совпадают с соответствующими атрибутами ОСС;
</li>
</ul>
<h2>Конституента</h2> <h2>Конституента</h2>
<p> <p>
@ -37,13 +91,84 @@ function HelpThesaurus() {
являются Термин, Конвенция, Типизация (Структура), Формальное определение, Текстовое определение, Комментарий. являются Термин, Конвенция, Типизация (Структура), Формальное определение, Текстовое определение, Комментарий.
</p> </p>
<ul> <ul>
По <b>наличию формального определения в рамках КС</b> выделены: По <b>характеру формального определения в рамках КС</b> выделены классы:
<li> <li>
базовое понятие (<i>неопределяемое понятие</i>) не имеет определения и задано конвенцией и аксиомами; базовое понятие (<i>неопределяемое понятие</i>) не имеет определения и задано конвенцией и аксиомами;
</li> </li>
<li> <li>
производное понятие (<i>выводимое понятие</i>) имеет определение. производное понятие (<i>выводимое понятие</i>) имеет определение.
</li> </li>
<li>утверждение определяется через логическое выражение.</li>
<li>шаблон определения содержит несвязанный параметр в определении.</li>
</ul>
<br />
<ul>
По <b>назначению</b> выделены типы конституент:
<li>
<IconCstBaseSet size='1rem' className='inline-icon' />
{'\u2009'}базисное множество (X#) представляет неопределяемое понятие, представленное структурой множества,
чьи элементы различимы и не сравнимы с элементами других базисных множеств;
</li>
<li>
<IconCstConstSet size='1rem' className='inline-icon' />
{'\u2009'}константное множество (C#) представляет неопределяемое понятие, моделируемое термом теории множеств,
который поддерживает ряд формальных операций над его элементами;
</li>
<li>
<IconCstStructured size='1rem' className='inline-icon' />
{'\u2009'}родовая структура (S#) представляет неопределяемое понятие, имеющее определенную структуру,
построенную на базисных множествах и константных множеств. Содержание родовой структуры формируется{' '}
<LinkTopic text='отношением типизации' topic={HelpTopic.RSL_TYPES} />, аксиомами и конвенцией;
</li>
<li>
<IconCstAxiom size='1rem' className='inline-icon' />
{'\u2009'}аксиома (A#) представляет утверждение, ограничивающее неопределяемые понятия и выводимые термы.
Интерпретация аксиомы должна быть истинна и является критерием корректности интерпретации КС в целом;
</li>
<li>
<IconCstTerm size='1rem' className='inline-icon' />
{'\u2009'}терм (D#) представляет выводимое понятие через формальное определение;
</li>
<li>
<IconCstFunction size='1rem' className='inline-icon' />
{'\u2009'}терм-функция (F#) представляет выводимое понятие (возможно параметризованное), имеющее характер
функционального отношения между набором аргументов и результатом;
</li>
<li>
<IconCstPredicate size='1rem' className='inline-icon' />
{'\u2009'}предикат-функция (P#) представляет выводимое понятие (возможно параметризованное), имеющее характер
логического выражения, проверяющее заданные аргументы на соответствие некоторому условию;
</li>
<li>
<IconCstTheorem size='1rem' className='inline-icon' />
{'\u2009'}теорема (T#) представляет ценное для предметной утверждение, значение которого может быть как
истинным так и ложным;
</li>
</ul>
<br />
<ul>
По <b>графу термов</b> выделены:
<li>
<IconGraphOutputs size='1rem' className='inline-icon' />
{'\u2009'}потребители данной конституенты конституенты, определения которых используют данную конституенту
</li>
<li>
<IconGraphInputs size='1rem' className='inline-icon' />
{'\u2009'}поставщики данной конституенты конституенты, имена которых используются в определении данной
конституенты
</li>
<li>
<IconGraphExpand size='1rem' className='inline-icon' />
{'\u2009'}зависимые от данной конституенты потребители данной конституенты напрямую или по цепочке
</li>
<li>
<IconGraphCollapse size='1rem' className='inline-icon' />
{'\u2009'}влияющие на данную конституенту поставщики данной конституенты напрямую или по цепочке
</li>
</ul> </ul>
<br /> <br />
@ -63,6 +188,33 @@ function HelpThesaurus() {
<br /> <br />
<ul>
Для характеристики <b>корректности определения</b> введены статусы конституент:
<li>
<IconStatusUnknown size='1rem' className='inline-icon' />
{'\u2009'}не проверено требуется проверка формального определения (промежуточный статус);
</li>
<li>
<IconStatusOK size='1rem' className='inline-icon' />
{'\u2009'}корректно формальное определение корректно;
</li>
<li>
<IconStatusError size='1rem' className='inline-icon' />
{'\u2009'}ошибочно ошибка в формальном определении;
</li>
<li>
<IconStatusProperty size='1rem' className='inline-icon' />
{'\u2009'}неразмерное формальное определение задает невычислимое множество, для которого возможно вычислить
предикат проверки принадлежности;
</li>
<li>
<IconStatusIncalculable size='1rem' className='inline-icon' />
{'\u2009'}невычислимо формальное определение невозможно интерпретировать напрямую;
</li>
</ul>
<br />
<ul> <ul>
Для описания <b>отождествления</b> введены: Для описания <b>отождествления</b> введены:
<li>отождествляемые конституенты конституенты, состоящие в отождествлении;</li> <li>отождествляемые конституенты конституенты, состоящие в отождествлении;</li>
@ -78,40 +230,53 @@ function HelpThesaurus() {
<ul> <ul>
Для описания <b>наследования</b> конституент в рамках ОСС введены: Для описания <b>наследования</b> конституент в рамках ОСС введены:
<li> <li>
<IconChild size='1rem' className='inline-icon' /> наследованная конституента конституента, перенесенная из <IconChild size='1rem' className='inline-icon' />
другой КС в рамках операции синтеза; {'\u2009'}наследованная конституента конституента, перенесенная из другой КС в рамках операции синтеза;
</li> </li>
<li>собственная конституента конституента, не являющаяся наследником других конституент;</li>
<li> <li>
<IconPredecessor size='1rem' className='inline-icon' /> исходная конституента для данной конституенты <IconPredecessor size='1rem' className='inline-icon' />
собственная конституента, прямым или опосредованным наследником которой является данная конституента. {'\u2009'}собственная конституента конституента, не являющаяся наследником других конституент;
</li>
<li>
<IconPredecessor size='1rem' className='inline-icon' />
{'\u2009'}исходная конституента для данной конституенты собственная конституента, прямым или опосредованным
наследником которой является данная конституента.
</li>
</ul>
<h2>Операционная схема синтеза</h2>
<p>
<IconOSS size='1rem' className='inline-icon' />
{'\u2009'}
<LinkTopic text='Операционная схема синтеза' topic={HelpTopic.CC_OSS} /> (ОСС) система концептуальных схем,
связанных операциями синтеза.
</p>
<p>
Граф синтеза ориентированный граф, вершинами которого являются операции, а ребра указывают на использование
результата одной операции как аргумента другой операции.
</p>
<h2>Операция</h2>
<p>Операция выделенная часть ОСС, определяющая способ получения КС в рамках ОСС.</p>
<ul>
По <b>способу получения КС выделены</b>:
<li>
<IconDownload size='1rem' className='inline-icon' />
{'\u2009'}загрузка КС из библиотеки;
</li>
<li>
<IconSynthesis size='1rem' className='inline-icon' />
{'\u2009'}синтез концептуальных схем.ыф
</li> </li>
</ul> </ul>
<br /> <br />
<ul> <p>
По <b>назначению</b> выделены: <IconConsolidation className='inline-icon' />
<li> {'\u2009'}Ромбовидный синтез операция, где используются КС, имеющие общих предков.
базисное множество (X1) задает неопределяемое понятие, представленное структурой множества, чьи элементы </p>
различимы и не сравнимы с элементами других базисных множеств;
</li>
<li>
константное множество (C1) задает неопределяемое понятие, моделируемое термом теории множеств, который
поддерживает ряд формальных операций над его элементами;
</li>
<li>
родовая структура (S1) задает неопределяемое понятие, имеющее определенную структуру, построенную на базисных
множествах и константных множеств. Содержание родовой структуры формируется{' '}
<LinkTopic text='отношением типизации' topic={HelpTopic.RSL_TYPES} />, аксиомами и конвенцией;
</li>
</ul>
<h2>Операционная схема синтеза</h2>
<p>Раздел в разработке...</p>
<h2>Операция</h2>
<p>Раздел в разработке...</p>
</div> </div>
); );
} }

View File

@ -1,4 +1,12 @@
import { IconConsolidation, IconExecute, IconOSS } from '@/components/Icons'; import {
IconConsolidation,
IconDownload,
IconExecute,
IconOSS,
IconRSFormImported,
IconRSFormOwned,
IconSynthesis
} from '@/components/Icons';
import LinkTopic from '@/components/ui/LinkTopic'; import LinkTopic from '@/components/ui/LinkTopic';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
@ -16,9 +24,24 @@ function HelpConceptOSS() {
и отображается в форме <LinkTopic text='Графа синтеза' topic={HelpTopic.UI_OSS_GRAPH} />. и отображается в форме <LinkTopic text='Графа синтеза' topic={HelpTopic.UI_OSS_GRAPH} />.
</p> </p>
<p> <p>
Базовыми операциями ОСС являются загрузка и синтез. Схема может быть загружена из другой локации ( Базовыми операциями ОСС являются <IconDownload size='1rem' className='inline-icon' /> загрузка и{' '}
<b>внешняя КС</b>) или создана в ОСС (<b>собственная КС</b>). Загрузка схем, полученных синтезом в других ОСС не <IconSynthesis size='1rem' className='inline-icon' /> синтез. Схема может быть загружена из другой локации{' '}
допускается. Также запрещена повторная загрузка той же КС в рамках одной ОСС. <span className='text-nowrap'>
(<IconRSFormImported size='1rem' className='inline-icon' />
<b>внешняя КС</b>)
</span>{' '}
или создана в ОСС{' '}
<span className='text-nowrap'>
(<IconRSFormOwned size='1rem' className='inline-icon' />
<b>собственная КС</b>)
</span>
. Загрузка схем, полученных синтезом в других ОСС не допускается. Также запрещена повторная загрузка той же КС в
рамках одной ОСС.
</p>
<p>
При изменении расположения или владельца ОСС соответствующие атрибуты изменяются у собственных КС. Также при
удалении ОСС удаляются и все собственные КС. При удалении операции, собственная КС отвязывается от ОСС и
становится свободной КС.
</p> </p>
<p> <p>
Операция синтеза в рамках ОСС задаются набором операций-аргументов и <b>таблицей отождествлений</b> понятий из Операция синтеза в рамках ОСС задаются набором операций-аргументов и <b>таблицей отождествлений</b> понятий из

View File

@ -47,13 +47,16 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
onDestroy={onDestroy} onDestroy={onDestroy}
controller={controller} controller={controller}
/> />
<AnimateFade onKeyDown={handleInput} className={clsx('sm:w-fit mx-auto', 'flex flex-col sm:flex-row px-6')}> <AnimateFade
onKeyDown={handleInput}
className={clsx('md:w-fit md:max-w-fit max-w-[32rem]', 'mx-auto ', 'flex flex-col md:flex-row px-6')}
>
<FlexColumn className='px-3'> <FlexColumn className='px-3'>
<FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} /> <FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
<EditorLibraryItem item={schema} isModified={isModified} controller={controller} /> <EditorLibraryItem item={schema} isModified={isModified} controller={controller} />
</FlexColumn> </FlexColumn>
<OssStats stats={schema?.stats} /> {schema ? <OssStats stats={schema.stats} /> : null}
</AnimateFade> </AnimateFade>
</> </>
); );

View File

@ -100,7 +100,7 @@ function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
id='schema_alias' id='schema_alias'
required required
label='Сокращение' label='Сокращение'
className='w-[14rem]' className='w-[16rem]'
disabled={!controller.isMutable} disabled={!controller.isMutable}
value={alias} value={alias}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}

View File

@ -1,26 +1,56 @@
import Divider from '@/components/ui/Divider'; import clsx from 'clsx';
import LabeledValue from '@/components/ui/LabeledValue';
import { IconDownload, IconRSForm, IconRSFormImported, IconRSFormOwned, IconSynthesis } from '@/components/Icons';
import ValueStats from '@/components/ui/ValueStats';
import { IOperationSchemaStats } from '@/models/oss'; import { IOperationSchemaStats } from '@/models/oss';
interface OssStatsProps { interface OssStatsProps {
stats?: IOperationSchemaStats; stats: IOperationSchemaStats;
} }
function OssStats({ stats }: OssStatsProps) { function OssStats({ stats }: OssStatsProps) {
if (!stats) {
return null;
}
return ( return (
<div className='flex flex-col sm:gap-1 sm:ml-6 sm:mt-8 sm:w-[16rem]'> <div
<Divider margins='my-2' className='sm:hidden' /> className={clsx(
'mt-3 md:ml-5 md:mt-8 md:w-[15rem] w-[20rem] h-min mx-auto', // prettier: split-lines
'grid grid-cols-3 gap-1 justify-items-end'
)}
>
<div id='count_operations' className='w-fit flex gap-3 hover:cursor-default '>
<span>Всего</span>
<span>{stats.count_operations}</span>
</div>
<ValueStats
id='count_inputs'
icon={<IconDownload size='1.25rem' className='clr-text-primary' />}
value={stats.count_inputs}
title='Загрузка'
/>
<ValueStats
id='count_synthesis'
icon={<IconSynthesis size='1.25rem' className='clr-text-primary' />}
value={stats.count_synthesis}
title='Синтез'
/>
<LabeledValue id='count_all' label='Всего операций' text={stats.count_operations} /> <ValueStats
<LabeledValue id='count_inputs' label='Загрузка' text={stats.count_inputs} /> id='count_schemas'
<LabeledValue id='count_synthesis' label='Синтез' text={stats.count_synthesis} /> icon={<IconRSForm size='1.25rem' className='clr-text-primary' />}
value={stats.count_schemas}
<Divider margins='my-2' /> title='Прикрепленные схемы'
/>
<LabeledValue id='count_schemas' label='Прикрепленные схемы' text={stats.count_schemas} /> <ValueStats
id='count_owned'
icon={<IconRSFormOwned size='1.25rem' className='clr-text-primary' />}
value={stats.count_owned}
title='Собственные'
/>
<ValueStats
id='count_imported'
icon={<IconRSFormImported size='1.25rem' className='clr-text-primary' />}
value={stats.count_schemas - stats.count_owned}
title='Внешние'
/>
</div> </div>
); );
} }

View File

@ -117,7 +117,7 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
noBorder noBorder
noOutline noOutline
tabIndex={-1} tabIndex={-1}
title={'Редактирование'} title='Редактирование'
hideTitle={editMenu.isOpen} hideTitle={editMenu.isOpen}
className='h-full px-2' className='h-full px-2'
icon={<IconEdit2 size='1.25rem' className={controller.isMutable ? 'icon-green' : 'icon-red'} />} icon={<IconEdit2 size='1.25rem' className={controller.isMutable ? 'icon-green' : 'icon-red'} />}

View File

@ -91,7 +91,7 @@ function OssTabs() {
} }
const onDestroySchema = useCallback(() => { const onDestroySchema = useCallback(() => {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) { if (!schema || !window.confirm(prompts.deleteOSS)) {
return; return;
} }
destroyItem(schema.id, () => { destroyItem(schema.id, () => {

View File

@ -79,7 +79,7 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
} }
return ( return (
<div className='overflow-y-auto' style={{ maxHeight: panelHeight }}> <div className='overflow-y-auto min-h-[20rem]' style={{ maxHeight: panelHeight }}>
<ToolbarConstituenta <ToolbarConstituenta
activeCst={activeCst} activeCst={activeCst}
disabled={disabled} disabled={disabled}

View File

@ -79,8 +79,8 @@ function ToolbarConstituenta({
/> />
<MiniButton <MiniButton
title='Создать конституенту после данной' title='Создать конституенту после данной'
icon={<IconNewItem size={'1.25rem'} className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={disabled} disabled={!controller.isContentEditable || controller.isProcessing}
onClick={() => controller.createCst(activeCst?.cst_type, false)} onClick={() => controller.createCst(activeCst?.cst_type, false)}
/> />
<MiniButton <MiniButton

View File

@ -4,7 +4,7 @@ import { useIntl } from 'react-intl';
import { IconDateCreate, IconDateUpdate, IconEditor, IconFolder, IconOwner } from '@/components/Icons'; import { IconDateCreate, IconDateUpdate, IconEditor, IconFolder, IconOwner } from '@/components/Icons';
import InfoUsers from '@/components/info/InfoUsers'; import InfoUsers from '@/components/info/InfoUsers';
import SelectUser from '@/components/select/SelectUser'; import SelectUser from '@/components/select/SelectUser';
import IconValue from '@/components/ui/IconValue'; import ValueIcon from '@/components/ui/ValueIcon';
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 { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
@ -47,7 +47,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
<IconValue <ValueIcon
className='sm:mb-1 text-ellipsis max-w-[30rem]' className='sm:mb-1 text-ellipsis max-w-[30rem]'
icon={<IconFolder size='1.25rem' className='icon-primary' />} icon={<IconFolder size='1.25rem' className='icon-primary' />}
value={item.location} value={item.location}
@ -68,7 +68,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
) : null} ) : null}
</Overlay> </Overlay>
) : null} ) : null}
<IconValue <ValueIcon
className='sm:mb-1' className='sm:mb-1'
icon={<IconOwner size='1.25rem' className='icon-primary' />} icon={<IconOwner size='1.25rem' className='icon-primary' />}
value={getUserLabel(item.owner)} value={getUserLabel(item.owner)}
@ -78,12 +78,11 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
/> />
<div className='sm:mb-1 flex justify-between items-center'> <div className='sm:mb-1 flex justify-between items-center'>
<IconValue <ValueIcon
id='editor_stats' id='editor_stats'
dense dense
icon={<IconEditor size='1.25rem' className='icon-primary' />} icon={<IconEditor size='1.25rem' className='icon-primary' />}
value={item.editors.length} value={item.editors.length}
title='Редакторы'
onClick={controller.promptEditors} onClick={controller.promptEditors}
disabled={isModified || controller.isProcessing || accessLevel < UserLevel.OWNER} disabled={isModified || controller.isProcessing || accessLevel < UserLevel.OWNER}
/> />
@ -91,7 +90,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} /> <InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} />
</Tooltip> </Tooltip>
<IconValue <ValueIcon
dense dense
disabled disabled
icon={<IconDateUpdate size='1.25rem' className='clr-text-green' />} icon={<IconDateUpdate size='1.25rem' className='clr-text-green' />}
@ -99,7 +98,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
title='Дата обновления' title='Дата обновления'
/> />
<IconValue <ValueIcon
dense dense
disabled disabled
icon={<IconDateCreate size='1.25rem' className='clr-text-green' />} icon={<IconDateCreate size='1.25rem' className='clr-text-green' />}

View File

@ -20,7 +20,7 @@ interface EditorRSFormCardProps {
} }
function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSFormCardProps) { function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSFormCardProps) {
const { schema } = useRSForm(); const model = useRSForm();
const controller = useRSEdit(); const controller = useRSEdit();
function initiateSubmit() { function initiateSubmit() {
@ -49,14 +49,14 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
/> />
<AnimateFade <AnimateFade
onKeyDown={handleInput} onKeyDown={handleInput}
className={clsx('sm:w-fit sm:max-w-fit max-w-[32rem]', 'mx-auto ', 'flex flex-col sm:flex-row px-6')} className={clsx('md:w-fit md:max-w-fit max-w-[32rem] mx-auto', 'flex flex-col md:flex-row px-6')}
> >
<FlexColumn className='flex-shrink'> <FlexColumn className='flex-shrink'>
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} /> <FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
<EditorLibraryItem item={schema} isModified={isModified} controller={controller} /> <EditorLibraryItem item={model.schema} isModified={isModified} controller={controller} />
</FlexColumn> </FlexColumn>
<RSFormStats stats={schema?.stats} /> {model.schema ? <RSFormStats stats={model.schema.stats} isArchive={model.isArchive} /> : null}
</AnimateFade> </AnimateFade>
</> </>
); );

View File

@ -100,7 +100,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
id='schema_alias' id='schema_alias'
required required
label='Сокращение' label='Сокращение'
className='w-[14rem]' className='w-[16rem]'
disabled={!controller.isContentEditable} disabled={!controller.isContentEditable}
value={alias} value={alias}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}

View File

@ -1,59 +1,151 @@
import Divider from '@/components/ui/Divider'; import clsx from 'clsx';
import LabeledValue from '@/components/ui/LabeledValue';
import {
IconChild,
IconConvention,
IconCstAxiom,
IconCstBaseSet,
IconCstConstSet,
IconCstFunction,
IconCstPredicate,
IconCstStructured,
IconCstTerm,
IconCstTheorem,
IconDefinition,
IconPredecessor,
IconStatusError,
IconStatusIncalculable,
IconStatusOK,
IconStatusProperty,
IconTerminology
} from '@/components/Icons';
import ValueStats from '@/components/ui/ValueStats';
import { type IRSFormStats } from '@/models/rsform'; import { type IRSFormStats } from '@/models/rsform';
interface RSFormStatsProps { interface RSFormStatsProps {
stats?: IRSFormStats; isArchive: boolean;
stats: IRSFormStats;
} }
function RSFormStats({ stats }: RSFormStatsProps) { function RSFormStats({ stats, isArchive }: RSFormStatsProps) {
if (!stats) {
return null;
}
return ( return (
<div className='flex flex-col sm:gap-1 sm:ml-6 sm:mt-8 sm:w-[16rem]'> <div
<Divider margins='my-2' className='sm:hidden' /> className={clsx(
'mt-3 md:ml-5 md:mt-8 md:w-[18rem] w-[25rem] h-min mx-auto', // prettier: split-lines
'grid grid-cols-4 gap-1 justify-items-end'
)}
>
<div id='count_all' className='col-span-2 w-fit flex gap-3 hover:cursor-default '>
<span>Всего</span>
<span>{stats.count_all}</span>
</div>
<ValueStats
id='count_owned'
icon={<IconPredecessor size='1.25rem' className='clr-text-primary' />}
value={stats.count_all - stats.count_inherited}
title='Собственные'
/>
<ValueStats
id='count_inherited'
icon={<IconChild size='1.25rem' className='clr-text-primary' />}
value={stats.count_inherited}
titleHtml={isArchive ? 'Архивные схемы не хранят<br/> информацию о наследовании' : 'Наследованные'}
/>
<LabeledValue id='count_all' label='Всего конституент' text={stats.count_all} /> <ValueStats
{stats.count_inherited !== 0 ? ( className='col-start-1'
<LabeledValue id='count_inherited' label='Наследованные' text={stats.count_inherited} /> id='count_ok'
) : null} icon={<IconStatusOK size='1.25rem' className='clr-text-green' />}
<LabeledValue id='count_errors' label='Некорректные' text={stats.count_errors} /> value={stats.count_all - stats.count_errors - stats.count_property - stats.count_incalculable}
{stats.count_property !== 0 ? ( title='Корректные'
<LabeledValue id='count_property' label='Неразмерные' text={stats.count_property} /> />
) : null} <ValueStats
{stats.count_incalculable !== 0 ? ( id='count_property'
<LabeledValue id='count_incalculable' label='Невычислимые' text={stats.count_incalculable} /> icon={<IconStatusProperty size='1.25rem' className='clr-text-primary' />}
) : null} value={stats.count_errors}
title='Неразмерные'
/>
<ValueStats
id='count_incalculable'
icon={<IconStatusIncalculable size='1.25rem' className='clr-text-red' />}
value={stats.count_incalculable}
title='Невычислимые'
/>
<ValueStats
id='count_errors'
icon={<IconStatusError size='1.25rem' className='clr-text-red' />}
value={stats.count_errors}
title='Некорректные'
/>
<Divider margins='my-2' /> <ValueStats
id='count_base'
icon={<IconCstBaseSet size='1.25rem' className='clr-text-controls' />}
value={stats.count_base}
title='Базисные множества'
/>
<ValueStats
id='count_constant'
icon={<IconCstConstSet size='1.25rem' className='clr-text-controls' />}
value={stats.count_constant}
title='Константные множества'
/>
<ValueStats
id='count_structured'
icon={<IconCstStructured size='1.25rem' className='clr-text-controls' />}
value={stats.count_structured}
title='Родовые структуры'
/>
<ValueStats
id='count_axiom'
icon={<IconCstAxiom size='1.25rem' className='clr-text-controls' />}
value={stats.count_axiom}
title='Аксиомы'
/>
<LabeledValue id='count_text_term' label='Термины' text={stats.count_text_term} /> <ValueStats
<LabeledValue id='count_definition' label='Определения' text={stats.count_definition} /> id='count_term'
<LabeledValue id='count_convention' label='Конвенции' text={stats.count_convention} /> icon={<IconCstTerm size='1.25rem' className='clr-text-controls' />}
value={stats.count_term}
title='Термы'
/>
<ValueStats
id='count_function'
icon={<IconCstFunction size='1.25rem' className='clr-text-controls' />}
value={stats.count_function}
title='Терм-функции'
/>
<ValueStats
id='count_predicate'
icon={<IconCstPredicate size='1.25rem' className='clr-text-controls' />}
value={stats.count_predicate}
title='Предикат-функции'
/>
<ValueStats
id='count_theorem'
icon={<IconCstTheorem size='1.25rem' className='clr-text-controls' />}
value={stats.count_theorem}
title='Теоремы'
/>
<Divider margins='my-2' /> <ValueStats
id='count_text_term'
{stats.count_base !== 0 ? ( icon={<IconTerminology size='1.25rem' className='clr-text-primary' />}
<LabeledValue id='count_base' label='Базисные множества ' text={stats.count_base} /> value={stats.count_text_term}
) : null} title='Термины'
{stats.count_constant !== 0 ? ( />
<LabeledValue id='count_constant' label='Константные множества ' text={stats.count_constant} /> <ValueStats
) : null} id='count_definition'
{stats.count_structured !== 0 ? ( icon={<IconDefinition size='1.25rem' className='clr-text-primary' />}
<LabeledValue id='count_structured' label='Родовые структуры ' text={stats.count_structured} /> value={stats.count_definition}
) : null} title='Определения'
{stats.count_axiom !== 0 ? <LabeledValue id='count_axiom' label='Аксиомы ' text={stats.count_axiom} /> : null} />
{stats.count_term !== 0 ? <LabeledValue id='count_term' label='Термы ' text={stats.count_term} /> : null} <ValueStats
{stats.count_function !== 0 ? ( id='count_convention'
<LabeledValue id='count_function' label='Терм-функции ' text={stats.count_function} /> icon={<IconConvention size='1.25rem' className='clr-text-primary' />}
) : null} value={stats.count_convention}
{stats.count_predicate !== 0 ? ( title='Конвенции'
<LabeledValue id='count_predicate' label='Предикат-функции ' text={stats.count_predicate} /> />
) : null}
{stats.count_theorem !== 0 ? (
<LabeledValue id='count_theorem' label='Теоремы ' text={stats.count_theorem} />
) : null}
</div> </div>
); );
} }

View File

@ -1,13 +1,20 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useLayoutEffect, useMemo, useState } from 'react'; import fileDownload from 'js-file-download';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import { IconCSV } from '@/components/Icons';
import SelectedCounter from '@/components/info/SelectedCounter'; import SelectedCounter from '@/components/info/SelectedCounter';
import { type RowSelectionState } from '@/components/ui/DataTable'; import { type RowSelectionState } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { ConstituentaID, CstType } from '@/models/rsform'; import { ConstituentaID, CstType } from '@/models/rsform';
import { information } from '@/utils/labels';
import { convertToCSV } from '@/utils/utils';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
import TableRSList from './TableRSList'; import TableRSList from './TableRSList';
@ -34,6 +41,19 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
} }
}, [controller.selected, controller.schema]); }, [controller.selected, controller.schema]);
const handleDownloadCSV = useCallback(() => {
if (!controller.schema || controller.schema.items.length === 0) {
toast.error(information.noDataToExport);
return;
}
const blob = convertToCSV(controller.schema.items);
try {
fileDownload(blob, `${controller.schema.alias}.csv`, 'text/csv;charset=utf-8;');
} catch (error) {
console.error(error);
}
}, [controller]);
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) { function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
if (!controller.schema) { if (!controller.schema) {
controller.deselectAll(); controller.deselectAll();
@ -121,6 +141,14 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
})} })}
/> />
<Overlay position='top-[0.25rem] right-[1rem]' layer='z-tooltip'>
<MiniButton
title='Выгрузить в формате CSV'
icon={<IconCSV size='1.25rem' className='icon-green' />}
onClick={handleDownloadCSV}
/>
</Overlay>
<TableRSList <TableRSList
items={controller.schema?.items} items={controller.schema?.items}
maxHeight={tableHeight} maxHeight={tableHeight}

View File

@ -207,7 +207,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
noBorder noBorder
noOutline noOutline
tabIndex={-1} tabIndex={-1}
title={'Редактирование'} title='Редактирование'
hideTitle={editMenu.isOpen} hideTitle={editMenu.isOpen}
className='h-full px-2' className='h-full px-2'
icon={<IconEdit2 size='1.25rem' className={controller.isContentEditable ? 'icon-green' : 'icon-red'} />} icon={<IconEdit2 size='1.25rem' className={controller.isContentEditable ? 'icon-green' : 'icon-red'} />}

View File

@ -134,7 +134,7 @@ div:not(.dense) > p {
} }
li::marker { li::marker {
content: ' '; content: '\2009';
} }
.border { .border {

View File

@ -6,7 +6,7 @@
* Global application Parameters. The place where magic numbers are put to rest. * Global application Parameters. The place where magic numbers are put to rest.
*/ */
export const PARAMETER = { export const PARAMETER = {
smallScreen: 640, // == tailwind:xs smallScreen: 640, // == tailwind:sm
smallTreeNodes: 50, // amount of nodes threshold for size increase for large graphs smallTreeNodes: 50, // amount of nodes threshold for size increase for large graphs
refreshTimeout: 100, // milliseconds delay for post-refresh actions refreshTimeout: 100, // milliseconds delay for post-refresh actions
minimalTimeout: 10, // milliseconds delay for fast updates minimalTimeout: 10, // milliseconds delay for fast updates
@ -30,6 +30,8 @@ export const PARAMETER = {
ossLongLabel: 14, // characters - threshold for long labels - small font ossLongLabel: 14, // characters - threshold for long labels - small font
ossTruncateLabel: 28, // characters - threshold for long labels - truncate ossTruncateLabel: 28, // characters - threshold for long labels - truncate
statSmallThreshold: 3, // characters - threshold for small labels - small font
logicLabel: 'LOGIC', logicLabel: 'LOGIC',
exteorVersion: '4.9.3', exteorVersion: '4.9.3',

View File

@ -933,6 +933,7 @@ export const information = {
versionRestored: 'Загрузка версии завершена', versionRestored: 'Загрузка версии завершена',
locationRenamed: 'Ваши схемы перемещены', locationRenamed: 'Ваши схемы перемещены',
cloneComplete: (alias: string) => `Копия создана: ${alias}`, cloneComplete: (alias: string) => `Копия создана: ${alias}`,
noDataToExport: 'Нет данных для экспорта',
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`, addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
newLibraryItem: 'Схема успешно создана', newLibraryItem: 'Схема успешно создана',
@ -976,6 +977,8 @@ export const tooltips = {
export const prompts = { export const prompts = {
promptUnsaved: 'Присутствуют несохраненные изменения. Продолжить без их учета?', promptUnsaved: 'Присутствуют несохраненные изменения. Продолжить без их учета?',
deleteLibraryItem: 'Вы уверены, что хотите удалить данную схему?', deleteLibraryItem: 'Вы уверены, что хотите удалить данную схему?',
deleteOSS:
'Внимание!!\nУдаление операционной схемы приведет к удалению всех операций и собственных концептуальных схем.\nДанное действие нельзя отменить.\nВы уверены, что хотите удалить данную ОСС?',
generateWordforms: 'Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?', generateWordforms: 'Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?',
restoreArchive: 'При восстановлении архивной версии актуальная схему будет заменена. Продолжить?', restoreArchive: 'При восстановлении архивной версии актуальная схему будет заменена. Продолжить?',
ownerChange: ownerChange:

View File

@ -169,3 +169,34 @@ export function extractErrorMessage(error: Error | AxiosError): string {
} }
return error.message; return error.message;
} }
/**
* Convert array of objects to CSV Blob.
*/
export function convertToCSV(targetObj: object[]): Blob {
if (!targetObj || targetObj.length === 0) {
return new Blob([], { type: 'text/csv;charset=utf-8;' });
}
const separator = ',';
const keys = Object.keys(targetObj[0]);
const csvContent =
keys.join(separator) +
'\n' +
(targetObj as Record<string, string | Date | number>[])
.map(item => {
return keys
.map(k => {
let cell = item[k] === null || item[k] === undefined ? '' : item[k];
cell = cell instanceof Date ? cell.toLocaleString() : cell.toString().replace(/"/g, '""');
if (cell.search(/("|,|\n)/g) >= 0) {
cell = `"${cell}"`;
}
return cell;
})
.join(separator);
})
.join('\n');
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
}