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
[Functionality - PROGRESS]
- Design first user experience
- Private projects. Consider cooperative editing
- OSS change propagation: Advanced features
[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
- 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
- replace reagraph with react-flow in TermGraph and FormulaGraph
- Search functionality for Help Manuals
- Export PDF (Items list, Graph)
- ARIA (accessibility considerations) - for now machine reading not supported
- 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
[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]
- 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
- move autopep8 and isort settings from vscode settings to pyproject.toml
- 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'])
def perform_destroy(self, instance: m.LibraryItem) -> None:
PropagationFacade.before_delete_schema(instance)
return super().perform_destroy(instance)
if instance.item_type == m.LibraryItemType.RSFORM:
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):
if self.action in ['update', 'partial_update']:

View File

@ -1,5 +1,5 @@
''' 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.rsform.models import RSForm
from apps.users.models import User
@ -55,7 +55,7 @@ class TestChangeAttributes(EndpointTester):
)
self.owned.execute_operation(self.operation3)
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')
@ -71,7 +71,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.owned.model.owner, self.user3)
self.assertEqual(self.ks1.model.owner, self.user)
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')
def test_set_location(self):
@ -86,7 +86,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.owned.model.location, data['location'])
self.assertNotEqual(self.ks1.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')
def test_set_access_policy(self):
@ -101,13 +101,13 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.owned.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.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')
def test_set_editors(self):
Editor.set(self.owned.model.pk, [self.user2.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]}
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.ks1.model.editors()), [self.user, self.user2])
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')
def test_sync_from_result(self):
@ -147,6 +147,13 @@ class TestChangeAttributes(EndpointTester):
response = self.executeOK(data=data, item=self.owned_id)
self.ks3.refresh_from_db()
self.assertEqual(self.ks3.alias, data['item_data']['alias'])
self.assertEqual(self.ks3.title, data['item_data']['title'])
self.assertEqual(self.ks3.comment, data['item_data']['comment'])
self.assertEqual(self.ks3.model.alias, data['item_data']['alias'])
self.assertEqual(self.ks3.model.title, data['item_data']['title'])
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__'
def to_representation(self, instance: LibraryItem) -> dict:
result = LibraryItemDetailsSerializer(instance).data
result['items'] = []
for cst in RSForm(instance).constituents().order_by('order'):
result['items'].append(CstSerializer(cst).data)
result['inheritance'] = []
result = self.to_base_data(instance)
for link in Inheritance.objects.filter(Q(child__schema=instance) | Q(parent__schema=instance)):
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['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'):
result['oss'].append({
'id': oss.pk,
@ -132,11 +137,9 @@ class RSFormSerializer(serializers.ModelSerializer):
def to_versioned_data(self) -> dict:
''' 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['editors']
del result['inheritance']
del result['oss']
del result['owner']
del result['visible']
@ -150,7 +153,7 @@ class RSFormSerializer(serializers.ModelSerializer):
def from_versioned_data(self, version: int, data: dict) -> dict:
''' Load data from version. '''
result = self.to_representation(cast(LibraryItem, self.instance))
result = self.to_base_data(cast(LibraryItem, self.instance))
result['version'] = version
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 { TbCalendarPlus as IconDateCreate } from 'react-icons/tb';
export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
export { PiFileCsv as IconCSV } from 'react-icons/pi';
// ==== User status =======
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 { TbHexagons as IconOSS } from 'react-icons/tb';
export { TbHexagon as IconRSForm } from 'react-icons/tb';
export { TbTopologyRing as IconConsolidation } from 'react-icons/tb';
export { GrInherit as IconChild } from 'react-icons/gr';
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
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 { TbTopologyRing as IconConsolidation } from 'react-icons/tb';
export { BiSpa as IconPredecessor } from 'react-icons/bi';
export { LuArchive as IconArchive } 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 { BiCheckCircle as IconStatusOK } 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 { 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 { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
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 { LuWand2 as IconGenerateNames } from 'react-icons/lu';
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}>
<Label text={label} />
<CodeMirror
className={'font-math'}
className='font-math'
id={id}
ref={thisRef}
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}
>
{icon ? icon : null}
{text ? <div className={'whitespace-nowrap'}>{text}</div> : null}
{text ? <div className='whitespace-nowrap'>{text}</div> : null}
</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';
interface LabeledValueProps extends CProps.Styling {
interface ValueLabeledProps extends CProps.Styling {
id?: string;
label: string;
text: string | number;
title?: string;
}
function LabeledValue({ id, label, text, title, className, ...restProps }: LabeledValueProps) {
function ValueLabeled({ id, label, text, title, className, ...restProps }: ValueLabeledProps) {
return (
<div className={clsx('flex justify-between gap-6', className)} {...restProps}>
<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
id='operation_alias'
label='Сокращение'
className='w-[14rem]'
className='w-[16rem]'
value={alias}
onChange={event => setAlias(event.target.value)}
disabled={attachedID !== undefined}

View File

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

View File

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

View File

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

View File

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

View File

@ -90,7 +90,8 @@ export class OssLoader {
count_operations: items.length,
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).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_synthesis: number;
count_schemas: number;
count_owned: number;
}
/**

View File

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

View File

@ -1,9 +1,13 @@
'use client';
import { AnimatePresence } from 'framer-motion';
import fileDownload from 'js-file-download';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
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 { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
@ -13,7 +17,7 @@ import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/librar
import { ILibraryFilter } from '@/models/miscellaneous';
import { storage } from '@/utils/constants';
import { information } from '@/utils/labels';
import { toggleTristateFlag } from '@/utils/utils';
import { convertToCSV, toggleTristateFlag } from '@/utils/utils';
import TableLibraryItems from './TableLibraryItems';
import ToolbarSearch from './ToolbarSearch';
@ -101,6 +105,19 @@ function LibraryPage() {
[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(
() => (
<TableLibraryItems
@ -142,6 +159,13 @@ function LibraryPage() {
hideWindow={() => setShowRenameLocation(false)}
/>
) : 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
total={library.items.length ?? 0}
filtered={items.length}

View File

@ -37,13 +37,14 @@ function HelpMain() {
<h2>Разделы Справки</h2>
{[
HelpTopic.INFO,
HelpTopic.THESAURUS,
HelpTopic.INTERFACE,
HelpTopic.CONCEPTUAL,
HelpTopic.RSLANG,
HelpTopic.TERM_CONTROL,
HelpTopic.ACCESS,
HelpTopic.VERSIONS,
HelpTopic.INFO,
HelpTopic.EXTEOR
].map(topic => (
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
@ -72,7 +73,7 @@ function HelpMain() {
версию браузера в случае возникновения визуальных ошибок или проблем с производительностью.
</p>
<p>
Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять по email:{' '}
Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять на email:{' '}
<TextURL href={external_urls.mail_portal} text='portal@acconcept.ru' />
</p>
</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 { HelpTopic } from '@/models/miscellaneous';
@ -12,11 +39,11 @@ function HelpThesaurus() {
Справки через гиперссылки. Также указываются графические обозначения (иконки, цвета), используемые для
обозначения соответствующих сущностей в интерфейсе Портала.
</p>
<h2>Концептуализация</h2>
<p>Раздел в разработке...</p>
<h2>Концептуальная схема</h2>
<p>
<IconRSForm size='1rem' className='inline-icon' />{' '}
<IconRSForm size='1rem' className='inline-icon' />
{'\u2009'}
<LinkTopic text='Концептуальная схема' topic={HelpTopic.CC_SYSTEM} /> (<i>система определений, КС</i>)
совокупность отдельных понятий и утверждений, а также связей между ними, задаваемых определениями.
</p>
@ -28,6 +55,33 @@ function HelpThesaurus() {
Родоструктурная экспликация КС экспликация КС с помощью{' '}
<LinkTopic text='аппарата родов структур' topic={HelpTopic.RSLANG} />.
</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>
<p>
@ -37,13 +91,84 @@ function HelpThesaurus() {
являются Термин, Конвенция, Типизация (Структура), Формальное определение, Текстовое определение, Комментарий.
</p>
<ul>
По <b>наличию формального определения в рамках КС</b> выделены:
По <b>характеру формального определения в рамках КС</b> выделены классы:
<li>
базовое понятие (<i>неопределяемое понятие</i>) не имеет определения и задано конвенцией и аксиомами;
</li>
<li>
производное понятие (<i>выводимое понятие</i>) имеет определение.
</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>
<br />
@ -63,6 +188,33 @@ function HelpThesaurus() {
<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>
Для описания <b>отождествления</b> введены:
<li>отождествляемые конституенты конституенты, состоящие в отождествлении;</li>
@ -78,40 +230,53 @@ function HelpThesaurus() {
<ul>
Для описания <b>наследования</b> конституент в рамках ОСС введены:
<li>
<IconChild size='1rem' className='inline-icon' /> наследованная конституента конституента, перенесенная из
другой КС в рамках операции синтеза;
<IconChild size='1rem' className='inline-icon' />
{'\u2009'}наследованная конституента конституента, перенесенная из другой КС в рамках операции синтеза;
</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>
</ul>
<br />
<ul>
По <b>назначению</b> выделены:
<li>
базисное множество (X1) задает неопределяемое понятие, представленное структурой множества, чьи элементы
различимы и не сравнимы с элементами других базисных множеств;
</li>
<li>
константное множество (C1) задает неопределяемое понятие, моделируемое термом теории множеств, который
поддерживает ряд формальных операций над его элементами;
</li>
<li>
родовая структура (S1) задает неопределяемое понятие, имеющее определенную структуру, построенную на базисных
множествах и константных множеств. Содержание родовой структуры формируется{' '}
<LinkTopic text='отношением типизации' topic={HelpTopic.RSL_TYPES} />, аксиомами и конвенцией;
</li>
</ul>
<h2>Операционная схема синтеза</h2>
<p>Раздел в разработке...</p>
<h2>Операция</h2>
<p>Раздел в разработке...</p>
<p>
<IconConsolidation className='inline-icon' />
{'\u2009'}Ромбовидный синтез операция, где используются КС, имеющие общих предков.
</p>
</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 { HelpTopic } from '@/models/miscellaneous';
@ -16,9 +24,24 @@ function HelpConceptOSS() {
и отображается в форме <LinkTopic text='Графа синтеза' topic={HelpTopic.UI_OSS_GRAPH} />.
</p>
<p>
Базовыми операциями ОСС являются загрузка и синтез. Схема может быть загружена из другой локации (
<b>внешняя КС</b>) или создана в ОСС (<b>собственная КС</b>). Загрузка схем, полученных синтезом в других ОСС не
допускается. Также запрещена повторная загрузка той же КС в рамках одной ОСС.
Базовыми операциями ОСС являются <IconDownload size='1rem' className='inline-icon' /> загрузка и{' '}
<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>
Операция синтеза в рамках ОСС задаются набором операций-аргументов и <b>таблицей отождествлений</b> понятий из

View File

@ -47,13 +47,16 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
onDestroy={onDestroy}
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'>
<FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
<EditorLibraryItem item={schema} isModified={isModified} controller={controller} />
</FlexColumn>
<OssStats stats={schema?.stats} />
{schema ? <OssStats stats={schema.stats} /> : null}
</AnimateFade>
</>
);

View File

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

View File

@ -1,26 +1,56 @@
import Divider from '@/components/ui/Divider';
import LabeledValue from '@/components/ui/LabeledValue';
import clsx from 'clsx';
import { IconDownload, IconRSForm, IconRSFormImported, IconRSFormOwned, IconSynthesis } from '@/components/Icons';
import ValueStats from '@/components/ui/ValueStats';
import { IOperationSchemaStats } from '@/models/oss';
interface OssStatsProps {
stats?: IOperationSchemaStats;
stats: IOperationSchemaStats;
}
function OssStats({ stats }: OssStatsProps) {
if (!stats) {
return null;
}
return (
<div className='flex flex-col sm:gap-1 sm:ml-6 sm:mt-8 sm:w-[16rem]'>
<Divider margins='my-2' className='sm:hidden' />
<div
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} />
<LabeledValue id='count_inputs' label='Загрузка' text={stats.count_inputs} />
<LabeledValue id='count_synthesis' label='Синтез' text={stats.count_synthesis} />
<Divider margins='my-2' />
<LabeledValue id='count_schemas' label='Прикрепленные схемы' text={stats.count_schemas} />
<ValueStats
id='count_schemas'
icon={<IconRSForm size='1.25rem' className='clr-text-primary' />}
value={stats.count_schemas}
title='Прикрепленные схемы'
/>
<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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ interface EditorRSFormCardProps {
}
function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSFormCardProps) {
const { schema } = useRSForm();
const model = useRSForm();
const controller = useRSEdit();
function initiateSubmit() {
@ -49,14 +49,14 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
/>
<AnimateFade
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'>
<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>
<RSFormStats stats={schema?.stats} />
{model.schema ? <RSFormStats stats={model.schema.stats} isArchive={model.isArchive} /> : null}
</AnimateFade>
</>
);

View File

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

View File

@ -1,59 +1,151 @@
import Divider from '@/components/ui/Divider';
import LabeledValue from '@/components/ui/LabeledValue';
import clsx from 'clsx';
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';
interface RSFormStatsProps {
stats?: IRSFormStats;
isArchive: boolean;
stats: IRSFormStats;
}
function RSFormStats({ stats }: RSFormStatsProps) {
if (!stats) {
return null;
}
function RSFormStats({ stats, isArchive }: RSFormStatsProps) {
return (
<div className='flex flex-col sm:gap-1 sm:ml-6 sm:mt-8 sm:w-[16rem]'>
<Divider margins='my-2' className='sm:hidden' />
<div
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} />
{stats.count_inherited !== 0 ? (
<LabeledValue id='count_inherited' label='Наследованные' text={stats.count_inherited} />
) : null}
<LabeledValue id='count_errors' label='Некорректные' text={stats.count_errors} />
{stats.count_property !== 0 ? (
<LabeledValue id='count_property' label='Неразмерные' text={stats.count_property} />
) : null}
{stats.count_incalculable !== 0 ? (
<LabeledValue id='count_incalculable' label='Невычислимые' text={stats.count_incalculable} />
) : null}
<ValueStats
className='col-start-1'
id='count_ok'
icon={<IconStatusOK size='1.25rem' className='clr-text-green' />}
value={stats.count_all - stats.count_errors - stats.count_property - stats.count_incalculable}
title='Корректные'
/>
<ValueStats
id='count_property'
icon={<IconStatusProperty size='1.25rem' className='clr-text-primary' />}
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} />
<LabeledValue id='count_definition' label='Определения' text={stats.count_definition} />
<LabeledValue id='count_convention' label='Конвенции' text={stats.count_convention} />
<ValueStats
id='count_term'
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' />
{stats.count_base !== 0 ? (
<LabeledValue id='count_base' label='Базисные множества ' text={stats.count_base} />
) : null}
{stats.count_constant !== 0 ? (
<LabeledValue id='count_constant' label='Константные множества ' text={stats.count_constant} />
) : null}
{stats.count_structured !== 0 ? (
<LabeledValue id='count_structured' label='Родовые структуры ' text={stats.count_structured} />
) : null}
{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}
{stats.count_function !== 0 ? (
<LabeledValue id='count_function' label='Терм-функции ' text={stats.count_function} />
) : null}
{stats.count_predicate !== 0 ? (
<LabeledValue id='count_predicate' label='Предикат-функции ' text={stats.count_predicate} />
) : null}
{stats.count_theorem !== 0 ? (
<LabeledValue id='count_theorem' label='Теоремы ' text={stats.count_theorem} />
) : null}
<ValueStats
id='count_text_term'
icon={<IconTerminology size='1.25rem' className='clr-text-primary' />}
value={stats.count_text_term}
title='Термины'
/>
<ValueStats
id='count_definition'
icon={<IconDefinition size='1.25rem' className='clr-text-primary' />}
value={stats.count_definition}
title='Определения'
/>
<ValueStats
id='count_convention'
icon={<IconConvention size='1.25rem' className='clr-text-primary' />}
value={stats.count_convention}
title='Конвенции'
/>
</div>
);
}

View File

@ -1,13 +1,20 @@
'use client';
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 { 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 { useConceptOptions } from '@/context/ConceptOptionsContext';
import { ConstituentaID, CstType } from '@/models/rsform';
import { information } from '@/utils/labels';
import { convertToCSV } from '@/utils/utils';
import { useRSEdit } from '../RSEditContext';
import TableRSList from './TableRSList';
@ -34,6 +41,19 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
}
}, [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>) {
if (!controller.schema) {
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
items={controller.schema?.items}
maxHeight={tableHeight}

View File

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

View File

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

View File

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

View File

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

View File

@ -169,3 +169,34 @@ export function extractErrorMessage(error: Error | AxiosError): string {
}
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;' });
}