Compare commits
9 Commits
07c2f1da2f
...
f86d847d64
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f86d847d64 | ||
![]() |
b60c1305bd | ||
![]() |
13b7b4d748 | ||
![]() |
eb5cb6945f | ||
![]() |
1305560679 | ||
![]() |
c864ec947b | ||
![]() |
7a4fceb9bf | ||
![]() |
e1a95e1d81 | ||
![]() |
cdefe97d98 |
32
TODO.txt
32
TODO.txt
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
if instance.item_type == m.LibraryItemType.RSFORM:
|
||||
PropagationFacade.before_delete_schema(instance)
|
||||
return super().perform_destroy(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']:
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
59
rsconcept/frontend/src/components/ui/ValueIcon.tsx
Normal file
59
rsconcept/frontend/src/components/ui/ValueIcon.tsx
Normal 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;
|
|
@ -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;
|
16
rsconcept/frontend/src/components/ui/ValueStats.tsx
Normal file
16
rsconcept/frontend/src/components/ui/ValueStats.tsx
Normal 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;
|
|
@ -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}
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
@ -42,7 +42,7 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
|
|||
<Modal
|
||||
header='Переименование конституенты'
|
||||
submitText='Переименовать'
|
||||
submitInvalidTooltip={'Введите незанятое имя, соответствующее типу'}
|
||||
submitInvalidTooltip='Введите незанятое имя, соответствующее типу'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={validated}
|
||||
onSubmit={() => onRename(cstData)}
|
||||
|
|
|
@ -30,7 +30,7 @@ function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCst
|
|||
<Modal
|
||||
header='Отождествление'
|
||||
submitText='Отождествить'
|
||||
submitInvalidTooltip={'Выберите две различные конституенты'}
|
||||
submitInvalidTooltip='Выберите две различные конституенты'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={canSubmit}
|
||||
onSubmit={handleSubmit}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -154,6 +154,7 @@ export interface IOperationSchemaStats {
|
|||
count_inputs: number;
|
||||
count_synthesis: number;
|
||||
count_schemas: number;
|
||||
count_owned: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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> понятий из
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'} />}
|
||||
|
|
|
@ -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, () => {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' />}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'} />}
|
||||
|
|
|
@ -134,7 +134,7 @@ div:not(.dense) > p {
|
|||
}
|
||||
|
||||
li::marker {
|
||||
content: '– ';
|
||||
content: '–\2009';
|
||||
}
|
||||
|
||||
.border {
|
||||
|
|
|
@ -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',
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;' });
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user