diff --git a/rsconcept/backend/apps/library/tests/s_views/t_versions.py b/rsconcept/backend/apps/library/tests/s_views/t_versions.py
index 5dab6978..25508aaa 100644
--- a/rsconcept/backend/apps/library/tests/s_views/t_versions.py
+++ b/rsconcept/backend/apps/library/tests/s_views/t_versions.py
@@ -84,15 +84,18 @@ class TestVersionViews(EndpointTester):
alias='A1',
cst_type='axiom',
definition_formal='X1=X1',
- order=1
+ order=1,
+ crucial=True
)
version_id = self._create_version({'version': '1.0.0', 'description': 'test'})
a1.definition_formal = 'X1=X2'
+ a1.crucial = False
a1.save()
response = self.executeOK(schema=self.owned_id, version=version_id)
loaded_a1 = response.data['items'][1]
self.assertEqual(loaded_a1['definition_formal'], 'X1=X1')
+ self.assertEqual(loaded_a1['crucial'], True)
self.assertEqual(loaded_a1['parse']['status'], 'verified')
diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py
index e72ed1b4..0813d8ea 100644
--- a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py
+++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py
@@ -121,7 +121,8 @@ class TestChangeConstituents(EndpointTester):
'term_raw': 'Test1',
'definition_formal': r'X4\X4',
'definition_raw': '@{X5|sing,datv}',
- 'convention': 'test'
+ 'convention': 'test',
+ 'crucial': True,
}
}
response = self.executeOK(data=data, schema=self.ks1.model.pk)
@@ -132,9 +133,11 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(self.ks1X1.definition_formal, data['item_data']['definition_formal'])
self.assertEqual(self.ks1X1.definition_raw, data['item_data']['definition_raw'])
self.assertEqual(self.ks1X1.convention, data['item_data']['convention'])
+ self.assertEqual(self.ks1X1.crucial, data['item_data']['crucial'])
self.assertEqual(d2.definition_resolved, data['item_data']['term_raw'])
self.assertEqual(inherited_cst.term_raw, data['item_data']['term_raw'])
self.assertEqual(inherited_cst.convention, data['item_data']['convention'])
+ self.assertEqual(inherited_cst.crucial, False)
self.assertEqual(inherited_cst.definition_formal, r'X1\X1')
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
diff --git a/rsconcept/backend/apps/rsform/admin.py b/rsconcept/backend/apps/rsform/admin.py
index d2d6d48c..ec4759fb 100644
--- a/rsconcept/backend/apps/rsform/admin.py
+++ b/rsconcept/backend/apps/rsform/admin.py
@@ -8,5 +8,5 @@ from . import models
class ConstituentaAdmin(admin.ModelAdmin):
''' Admin model: Constituenta. '''
ordering = ['schema', 'order']
- list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved']
+ list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved', 'crucial']
search_fields = ['term_resolved', 'definition_resolved']
diff --git a/rsconcept/backend/apps/rsform/migrations/0004_constituenta_crucial.py b/rsconcept/backend/apps/rsform/migrations/0004_constituenta_crucial.py
new file mode 100644
index 00000000..38b5a631
--- /dev/null
+++ b/rsconcept/backend/apps/rsform/migrations/0004_constituenta_crucial.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2.4 on 2025-07-29 09:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('rsform', '0003_alter_constituenta_order'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='constituenta',
+ name='crucial',
+ field=models.BooleanField(default=False, verbose_name='Ключевая'),
+ ),
+ ]
diff --git a/rsconcept/backend/apps/rsform/models/Constituenta.py b/rsconcept/backend/apps/rsform/models/Constituenta.py
index 8acffe2f..c845cd2a 100644
--- a/rsconcept/backend/apps/rsform/models/Constituenta.py
+++ b/rsconcept/backend/apps/rsform/models/Constituenta.py
@@ -4,6 +4,7 @@ import re
from cctext import extract_entities
from django.db.models import (
CASCADE,
+ BooleanField,
CharField,
ForeignKey,
JSONField,
@@ -103,6 +104,10 @@ class Constituenta(Model):
default='',
blank=True
)
+ crucial = BooleanField(
+ verbose_name='Ключевая',
+ default=False
+ )
class Meta:
''' Model metadata. '''
diff --git a/rsconcept/backend/apps/rsform/models/RSForm.py b/rsconcept/backend/apps/rsform/models/RSForm.py
index 1c721bec..39fae1f5 100644
--- a/rsconcept/backend/apps/rsform/models/RSForm.py
+++ b/rsconcept/backend/apps/rsform/models/RSForm.py
@@ -144,6 +144,7 @@ class RSForm:
self.cache.ensure_loaded()
position = self.cache.constituents.index(self.cache.by_id[insert_after.pk]) + 1
result = self.insert_new(data['alias'], data['cst_type'], position)
+ result.crucial = data.get('crucial', False)
result.convention = data.get('convention', '')
result.definition_formal = data.get('definition_formal', '')
result.term_forms = data.get('term_forms', [])
@@ -247,6 +248,9 @@ class RSForm:
else:
old_data['convention'] = cst.convention
cst.convention = data['convention']
+ if 'crucial' in data:
+ cst.crucial = data['crucial']
+ del data['crucial']
if 'definition_formal' in data:
if cst.definition_formal == data['definition_formal']:
del data['definition_formal']
diff --git a/rsconcept/backend/apps/rsform/serializers/__init__.py b/rsconcept/backend/apps/rsform/serializers/__init__.py
index 966c8f86..bc6e198f 100644
--- a/rsconcept/backend/apps/rsform/serializers/__init__.py
+++ b/rsconcept/backend/apps/rsform/serializers/__init__.py
@@ -12,6 +12,7 @@ from .basics import (
WordFormSerializer
)
from .data_access import (
+ CrucialUpdateSerializer,
CstCreateSerializer,
CstInfoSerializer,
CstListSerializer,
diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py
index 8a5063c2..e8c04eeb 100644
--- a/rsconcept/backend/apps/rsform/serializers/data_access.py
+++ b/rsconcept/backend/apps/rsform/serializers/data_access.py
@@ -46,12 +46,13 @@ class CstUpdateSerializer(StrictSerializer):
class Meta:
''' serializer metadata. '''
model = Constituenta
- fields = 'alias', 'cst_type', 'convention', 'definition_formal', 'definition_raw', 'term_raw', 'term_forms'
+ fields = 'alias', 'cst_type', 'convention', 'crucial', 'definition_formal', \
+ 'definition_raw', 'term_raw', 'term_forms'
target = PKField(
many=False,
queryset=Constituenta.objects.all().only(
- 'alias', 'cst_type', 'convention', 'definition_formal', 'definition_raw', 'term_raw')
+ 'alias', 'cst_type', 'convention', 'crucial', 'definition_formal', 'definition_raw', 'term_raw')
)
item_data = ConstituentaUpdateData()
@@ -71,6 +72,24 @@ class CstUpdateSerializer(StrictSerializer):
return attrs
+class CrucialUpdateSerializer(StrictSerializer):
+ ''' Serializer: update crucial status. '''
+ target = PKField(
+ many=True,
+ queryset=Constituenta.objects.all().only('crucial', 'schema_id')
+ )
+ value = serializers.BooleanField()
+
+ def validate(self, attrs):
+ schema = cast(LibraryItem, self.context['schema'])
+ for cst in attrs['target']:
+ if schema and cst.schema_id != schema.pk:
+ raise serializers.ValidationError({
+ f'{cst.pk}': msg.constituentaNotInRSform(schema.title)
+ })
+ return attrs
+
+
class CstDetailsSerializer(StrictModelSerializer):
''' Serializer: Constituenta data including parse. '''
parse = CstParseSerializer()
@@ -96,7 +115,7 @@ class CstCreateSerializer(StrictModelSerializer):
''' serializer metadata. '''
model = Constituenta
fields = \
- 'alias', 'cst_type', 'convention', \
+ 'alias', 'cst_type', 'convention', 'crucial', \
'term_raw', 'definition_raw', 'definition_formal', \
'insert_after', 'term_forms'
diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py
index f119d367..6c68f8aa 100644
--- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py
+++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py
@@ -225,7 +225,9 @@ class TestRSFormViewset(EndpointTester):
'cst_type': CstType.BASE,
'insert_after': x2.pk,
'term_raw': 'test',
- 'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}]
+ 'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}],
+ 'definition_formal': 'invalid',
+ 'crucial': True
}
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
@@ -233,6 +235,8 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(x4.order, 2)
self.assertEqual(x4.term_raw, data['term_raw'])
self.assertEqual(x4.term_forms, data['term_forms'])
+ self.assertEqual(x4.definition_formal, data['definition_formal'])
+ self.assertEqual(x4.crucial, data['crucial'])
data = {
'alias': 'X5',
@@ -574,6 +578,19 @@ class TestConstituentaAPI(EndpointTester):
self.assertEqual(self.cst3.definition_resolved, 'form1')
self.assertEqual(self.cst3.term_forms, data['item_data']['term_forms'])
+ @decl_endpoint('/api/rsforms/{schema}/update-crucial', method='patch')
+ def test_update_crucial(self):
+ data = {'target': [self.cst1.pk], 'value': True}
+ self.executeForbidden(data=data, schema=self.unowned_id)
+
+ self.logout()
+ self.executeForbidden(data=data, schema=self.owned_id)
+
+ self.login()
+ self.executeOK(data=data, schema=self.owned_id)
+ self.cst1.refresh_from_db()
+ self.assertEqual(self.cst1.crucial, True)
+
class TestInlineSynthesis(EndpointTester):
''' Testing Operations endpoints. '''
diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py
index 8cf6b726..ef0592ef 100644
--- a/rsconcept/backend/apps/rsform/views/rsforms.py
+++ b/rsconcept/backend/apps/rsform/views/rsforms.py
@@ -42,6 +42,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
'load_trs',
'create_cst',
'update_cst',
+ 'update_crucial',
'move_cst',
'delete_multiple_cst',
'substitute',
@@ -137,6 +138,36 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
data=s.RSFormParseSerializer(schema.model).data
)
+ @extend_schema(
+ summary='update crucial attributes of a given list of constituents',
+ tags=['RSForm'],
+ request=s.CrucialUpdateSerializer,
+ responses={
+ c.HTTP_200_OK: s.RSFormParseSerializer,
+ c.HTTP_400_BAD_REQUEST: None,
+ c.HTTP_403_FORBIDDEN: None,
+ c.HTTP_404_NOT_FOUND: None
+ }
+ )
+ @action(detail=True, methods=['patch'], url_path='update-crucial')
+ def update_crucial(self, request: Request, pk) -> HttpResponse:
+ ''' Update crucial attributes of a given list of constituents. '''
+ model = self._get_item()
+ serializer = s.CrucialUpdateSerializer(data=request.data, partial=True, context={'schema': model})
+ serializer.is_valid(raise_exception=True)
+ value: bool = serializer.validated_data['value']
+
+ with transaction.atomic():
+ for cst in serializer.validated_data['target']:
+ cst.crucial = value
+ cst.save(update_fields=['crucial'])
+ model.save(update_fields=['time_update'])
+
+ return Response(
+ status=c.HTTP_200_OK,
+ data=s.RSFormParseSerializer(model).data
+ )
+
@extend_schema(
summary='produce the structure of a given constituenta',
tags=['RSForm'],
diff --git a/rsconcept/frontend/src/components/control/text-button.tsx b/rsconcept/frontend/src/components/control/text-button.tsx
new file mode 100644
index 00000000..b941caba
--- /dev/null
+++ b/rsconcept/frontend/src/components/control/text-button.tsx
@@ -0,0 +1,37 @@
+import { globalIDs } from '@/utils/constants';
+
+import { type Button as ButtonStyle } from '../props';
+import { cn } from '../utils';
+
+interface TextButtonProps extends ButtonStyle {
+ /** Text to display second. */
+ text: string;
+}
+
+/**
+ * Customizable `button` with text, transparent background and no additional styling.
+ */
+export function TextButton({ text, title, titleHtml, hideTitle, className, ...restProps }: TextButtonProps) {
+ return (
+
+ {text}
+
+ );
+}
diff --git a/rsconcept/frontend/src/components/control/text-url.tsx b/rsconcept/frontend/src/components/control/text-url.tsx
index b4d11e62..05b5916b 100644
--- a/rsconcept/frontend/src/components/control/text-url.tsx
+++ b/rsconcept/frontend/src/components/control/text-url.tsx
@@ -30,7 +30,7 @@ export function TextURL({ text, href, title, color = 'text-primary', onClick }:
);
} else if (onClick) {
return (
-
+
{text}
);
diff --git a/rsconcept/frontend/src/components/icons.tsx b/rsconcept/frontend/src/components/icons.tsx
index f242cc80..a9300ff7 100644
--- a/rsconcept/frontend/src/components/icons.tsx
+++ b/rsconcept/frontend/src/components/icons.tsx
@@ -106,9 +106,9 @@ export { LuDatabase as IconDatabase } from 'react-icons/lu';
export { LuView as IconDBStructure } from 'react-icons/lu';
export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
export { LuImage as IconImage } from 'react-icons/lu';
-export { PiFediverseLogo as IconGraphSelection } from 'react-icons/pi';
export { GoVersions as IconVersions } from 'react-icons/go';
export { LuAtSign as IconTerm } from 'react-icons/lu';
+export { MdTaskAlt as IconCrucial } from 'react-icons/md';
export { LuSubscript as IconAlias } from 'react-icons/lu';
export { TbMathFunction as IconFormula } from 'react-icons/tb';
export { BiFontFamily as IconText } from 'react-icons/bi';
@@ -150,9 +150,11 @@ export { GrConnect as IconConnect } from 'react-icons/gr';
export { BiPlayCircle as IconExecute } from 'react-icons/bi';
// ======== Graph UI =======
+export { PiFediverseLogo as IconContextSelection } from 'react-icons/pi';
+export { ImMakeGroup as IconGroupSelection } from 'react-icons/im';
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
-export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
+export { TiArrowMaximise as IconGraphMaximize } from 'react-icons/ti';
export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
diff --git a/rsconcept/frontend/src/features/ai/models/prompting-api.ts b/rsconcept/frontend/src/features/ai/models/prompting-api.ts
index 792b4d26..3c2eb3be 100644
--- a/rsconcept/frontend/src/features/ai/models/prompting-api.ts
+++ b/rsconcept/frontend/src/features/ai/models/prompting-api.ts
@@ -40,12 +40,20 @@ export function generateSample(target: string): string {
export function varSchema(schema: IRSForm): string {
let result = `Название концептуальной схемы: ${schema.title}\n`;
result += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
- result += 'Понятия:\n';
+ result += 'Конституенты:\n';
schema.items.forEach(item => {
result += `\n${item.alias} - "${labelCstTypification(item)}" - "${item.term_resolved}" - "${
item.definition_formal
}" - "${item.definition_resolved}" - "${item.convention}"`;
});
+ if (schema.stats.count_crucial > 0) {
+ result +=
+ '\nКлючевые конституенты: ' +
+ schema.items
+ .filter(cst => cst.crucial)
+ .map(cst => cst.alias)
+ .join(', ');
+ }
return result;
}
diff --git a/rsconcept/frontend/src/features/help/items/help-thesaurus.tsx b/rsconcept/frontend/src/features/help/items/help-thesaurus.tsx
index 85be08f2..45789313 100644
--- a/rsconcept/frontend/src/features/help/items/help-thesaurus.tsx
+++ b/rsconcept/frontend/src/features/help/items/help-thesaurus.tsx
@@ -1,6 +1,7 @@
import {
IconChild,
IconConsolidation,
+ IconCrucial,
IconCstAxiom,
IconCstBaseSet,
IconCstConstSet,
@@ -91,6 +92,11 @@ export function HelpThesaurus() {
родоструктурной экспликации являются Термин, Конвенция, Типизация (Структура), Формальное определение, Текстовое
определение, Комментарий.
+
+ Ключевая конституента используется как маркер для
+ обозначения содержательно значимых конституент. Ключевые конституенты выделяются визуально и используются при
+ фильтрации.
+
diff --git a/rsconcept/frontend/src/features/help/items/ui/help-rseditor.tsx b/rsconcept/frontend/src/features/help/items/ui/help-rseditor.tsx
index f142f02c..8c140e5d 100644
--- a/rsconcept/frontend/src/features/help/items/ui/help-rseditor.tsx
+++ b/rsconcept/frontend/src/features/help/items/ui/help-rseditor.tsx
@@ -1,10 +1,10 @@
import {
IconChild,
IconClone,
+ IconContextSelection,
+ IconCrucial,
IconDestroy,
- IconEdit,
IconFilter,
- IconGraphSelection,
IconKeyboard,
IconLeftOpen,
IconMoveDown,
@@ -27,6 +27,13 @@ export function HelpRSEditor() {
return (
Редактор конституенты
+
+
+
+ статус ключевой конституенты
+
+
+
Команды
@@ -69,7 +76,7 @@ export function HelpRSEditor() {
фильтрация по атрибутам
- фильтрация по графу термов
+ фильтрация по графу термов
отображение наследованных
@@ -114,8 +121,7 @@ export function HelpRSEditor() {
Термин и Текстовое определение
- редактирование{' '}
- /{' '}
+ Клик редактирование /{' '}
diff --git a/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx b/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx
index 1199d7cd..51a7d397 100644
--- a/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx
+++ b/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx
@@ -1,6 +1,9 @@
import { Divider } from '@/components/container';
import {
+ IconChild,
IconClustering,
+ IconContextSelection,
+ IconCrucial,
IconDestroy,
IconEdit,
IconFilter,
@@ -12,7 +15,7 @@ import {
IconGraphInputs,
IconGraphMaximize,
IconGraphOutputs,
- IconGraphSelection,
+ IconGroupSelection,
IconNewItem,
IconOSS,
IconPredecessor,
@@ -103,7 +106,7 @@ export function HelpRSGraphTerm() {
Выделение
- выделить связанные...
+ выделить связанные...
все влияющие
@@ -120,13 +123,23 @@ export function HelpRSGraphTerm() {
исходящие напрямую
+
+ выделить группы...
+
выделить
+
+ выделить ключевые
+
выделить{' '}
+
+ выделить{' '}
+
+
diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-schema.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-schema.tsx
index 2a9952da..e00c9031 100644
--- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-schema.tsx
+++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-schema.tsx
@@ -77,6 +77,7 @@ export function ToolbarSchema({
const targetType = activeCst?.cst_type ?? CstType.BASE;
const data: ICreateConstituentaDTO = {
insert_after: activeCst?.id ?? null,
+ crucial: false,
cst_type: targetType,
alias: generateAlias(targetType, schema),
term_raw: '',
@@ -96,6 +97,7 @@ export function ToolbarSchema({
itemID: schema.id,
data: {
insert_after: activeCst.id,
+ crucial: activeCst.crucial,
cst_type: activeCst.cst_type,
alias: generateAlias(activeCst.cst_type, schema),
term_raw: activeCst.term_raw,
diff --git a/rsconcept/frontend/src/features/rsform/backend/api.ts b/rsconcept/frontend/src/features/rsform/backend/api.ts
index 7c415568..ad590f57 100644
--- a/rsconcept/frontend/src/features/rsform/backend/api.ts
+++ b/rsconcept/frontend/src/features/rsform/backend/api.ts
@@ -17,6 +17,7 @@ import {
type IRSFormUploadDTO,
type ISubstitutionsDTO,
type IUpdateConstituentaDTO,
+ type IUpdateCrucialDTO,
schemaConstituentaCreatedResponse,
schemaExpressionParse,
schemaProduceStructureResponse,
@@ -79,6 +80,15 @@ export const rsformsApi = {
successMessage: infoMsg.changesSaved
}
}),
+ updateCrucial: ({ itemID, data }: { itemID: number; data: IUpdateCrucialDTO }) =>
+ axiosPatch
({
+ schema: schemaRSForm,
+ endpoint: `/api/rsforms/${itemID}/update-crucial`,
+ request: {
+ data: data,
+ successMessage: infoMsg.changesSaved
+ }
+ }),
deleteConstituents: ({ itemID, data }: { itemID: number; data: IConstituentaList }) =>
axiosPatch({
schema: schemaRSForm,
diff --git a/rsconcept/frontend/src/features/rsform/backend/rsform-loader.ts b/rsconcept/frontend/src/features/rsform/backend/rsform-loader.ts
index 4aa22dab..50f5eae5 100644
--- a/rsconcept/frontend/src/features/rsform/backend/rsform-loader.ts
+++ b/rsconcept/frontend/src/features/rsform/backend/rsform-loader.ts
@@ -183,6 +183,7 @@ export class RSFormLoader {
const items = this.schema.items;
return {
count_all: items.length,
+ count_crucial: items.reduce((sum, cst) => sum + (cst.crucial ? 1 : 0), 0),
count_errors: items.reduce((sum, cst) => sum + (cst.parse.status === ParsingStatus.INCORRECT ? 1 : 0), 0),
count_property: items.reduce((sum, cst) => sum + (cst.parse.valueClass === ValueClass.PROPERTY ? 1 : 0), 0),
count_incalculable: items.reduce(
diff --git a/rsconcept/frontend/src/features/rsform/backend/types.ts b/rsconcept/frontend/src/features/rsform/backend/types.ts
index 04246e0f..80f477a7 100644
--- a/rsconcept/frontend/src/features/rsform/backend/types.ts
+++ b/rsconcept/frontend/src/features/rsform/backend/types.ts
@@ -65,6 +65,9 @@ export type IConstituentaCreatedResponse = z.infer;
+/** Represents data, used in batch updating crucial attributes in {@link IConstituenta}. */
+export type IUpdateCrucialDTO = z.infer;
+
/** Represents data, used in ordering a list of {@link IConstituenta}. */
export interface IMoveConstituentsDTO {
items: number[];
@@ -276,6 +279,7 @@ export const schemaConstituentaBasics = z.strictObject({
id: z.coerce.number(),
alias: z.string().nonempty(errorMsg.requiredField),
convention: z.string(),
+ crucial: z.boolean(),
cst_type: schemaCstType,
definition_formal: z.string(),
definition_raw: z.string(),
@@ -321,7 +325,8 @@ export const schemaVersionCreatedResponse = z.strictObject({
export const schemaCreateConstituenta = schemaConstituentaBasics
.pick({
cst_type: true,
- term_forms: true
+ term_forms: true,
+ crucial: true
})
.extend({
alias: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
@@ -342,6 +347,7 @@ export const schemaUpdateConstituenta = z.strictObject({
item_data: z.strictObject({
alias: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField).optional(),
cst_type: schemaCstType.optional(),
+ crucial: z.boolean().optional(),
convention: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
definition_formal: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
definition_raw: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
@@ -357,6 +363,11 @@ export const schemaUpdateConstituenta = z.strictObject({
})
});
+export const schemaUpdateCrucial = z.strictObject({
+ target: z.array(z.number()),
+ value: z.boolean()
+});
+
export const schemaProduceStructureResponse = z.strictObject({
cst_list: z.array(z.number()),
schema: schemaRSForm
diff --git a/rsconcept/frontend/src/features/rsform/backend/use-update-crucial.ts b/rsconcept/frontend/src/features/rsform/backend/use-update-crucial.ts
new file mode 100644
index 00000000..8707f25b
--- /dev/null
+++ b/rsconcept/frontend/src/features/rsform/backend/use-update-crucial.ts
@@ -0,0 +1,25 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
+
+import { KEYS } from '@/backend/configuration';
+
+import { rsformsApi } from './api';
+import { type IUpdateCrucialDTO } from './types';
+
+export const useUpdateCrucial = () => {
+ const client = useQueryClient();
+ const { updateTimestamp } = useUpdateTimestamp();
+ const mutation = useMutation({
+ mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'update-crucial'],
+ mutationFn: rsformsApi.updateCrucial,
+ onSuccess: data => {
+ updateTimestamp(data.id, data.time_update);
+ client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
+ },
+ onError: () => client.invalidateQueries()
+ });
+ return {
+ updateCrucial: (data: { itemID: number; data: IUpdateCrucialDTO }) => mutation.mutateAsync(data)
+ };
+};
diff --git a/rsconcept/frontend/src/features/rsform/components/badge-constituenta.tsx b/rsconcept/frontend/src/features/rsform/components/badge-constituenta.tsx
index be593edf..15169707 100644
--- a/rsconcept/frontend/src/features/rsform/components/badge-constituenta.tsx
+++ b/rsconcept/frontend/src/features/rsform/components/badge-constituenta.tsx
@@ -26,6 +26,7 @@ export function BadgeConstituenta({ value, prefixID }: BadgeConstituentaProps) {
className={clsx(
'cc-badge-constituenta',
value.is_inherited && 'border-dashed',
+ value.crucial && 'cc-badge-inner-shadow',
value.cst_class === CstClass.BASIC ? 'bg-accent-green25' : 'bg-input'
)}
style={{
diff --git a/rsconcept/frontend/src/features/rsform/components/icon-crucial-value.tsx b/rsconcept/frontend/src/features/rsform/components/icon-crucial-value.tsx
new file mode 100644
index 00000000..913a879d
--- /dev/null
+++ b/rsconcept/frontend/src/features/rsform/components/icon-crucial-value.tsx
@@ -0,0 +1,10 @@
+import { type DomIconProps, IconCrucial } from '@/components/icons';
+import { cn } from '@/components/utils';
+
+export function IconCrucialValue({ value, size = '1.25rem', className }: DomIconProps) {
+ if (value) {
+ return ;
+ } else {
+ return ;
+ }
+}
diff --git a/rsconcept/frontend/src/features/rsform/components/icon-dependency-mode.tsx b/rsconcept/frontend/src/features/rsform/components/icon-dependency-mode.tsx
index 85095af9..38bada9b 100644
--- a/rsconcept/frontend/src/features/rsform/components/icon-dependency-mode.tsx
+++ b/rsconcept/frontend/src/features/rsform/components/icon-dependency-mode.tsx
@@ -1,10 +1,10 @@
import {
type DomIconProps,
+ IconContextSelection,
IconGraphCollapse,
IconGraphExpand,
IconGraphInputs,
- IconGraphOutputs,
- IconGraphSelection
+ IconGraphOutputs
} from '@/components/icons';
import { DependencyMode } from '../stores/cst-search';
@@ -13,7 +13,7 @@ import { DependencyMode } from '../stores/cst-search';
export function IconDependencyMode({ value, size = '1.25rem', className }: DomIconProps) {
switch (value) {
case DependencyMode.ALL:
- return ;
+ return ;
case DependencyMode.OUTPUTS:
return ;
case DependencyMode.INPUTS:
diff --git a/rsconcept/frontend/src/features/rsform/components/info-constituenta.tsx b/rsconcept/frontend/src/features/rsform/components/info-constituenta.tsx
index d252ec46..e89d0e68 100644
--- a/rsconcept/frontend/src/features/rsform/components/info-constituenta.tsx
+++ b/rsconcept/frontend/src/features/rsform/components/info-constituenta.tsx
@@ -1,4 +1,4 @@
-import { IconChild } from '@/components/icons';
+import { IconChild, IconCrucial } from '@/components/icons';
import { cn } from '@/components/utils';
import { labelCstTypification } from '../labels';
@@ -15,6 +15,7 @@ export function InfoConstituenta({ data, className, ...restProps }: InfoConstitu
{data.alias}
{data.is_inherited ? : null}
+ {data.crucial ? : null}
{data.term_resolved ? (
diff --git a/rsconcept/frontend/src/features/rsform/components/pick-multi-constituenta.tsx b/rsconcept/frontend/src/features/rsform/components/pick-multi-constituenta.tsx
index a0cd840c..6056afef 100644
--- a/rsconcept/frontend/src/features/rsform/components/pick-multi-constituenta.tsx
+++ b/rsconcept/frontend/src/features/rsform/components/pick-multi-constituenta.tsx
@@ -116,7 +116,8 @@ export function PickMultiConstituenta({
const cst = schema.cstByID.get(cstID);
return !!cst && isBasicConcept(cst.cst_type);
}}
- isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
+ isCrucial={cstID => schema.cstByID.get(cstID)?.crucial ?? false}
+ isInherited={cstID => schema.cstByID.get(cstID)?.is_inherited ?? false}
value={value}
onChange={onChange}
className='w-fit'
diff --git a/rsconcept/frontend/src/features/rsform/components/rsform-stats.tsx b/rsconcept/frontend/src/features/rsform/components/rsform-stats.tsx
index f756a623..1b89d0ee 100644
--- a/rsconcept/frontend/src/features/rsform/components/rsform-stats.tsx
+++ b/rsconcept/frontend/src/features/rsform/components/rsform-stats.tsx
@@ -1,6 +1,7 @@
import {
IconChild,
IconConvention,
+ IconCrucial,
IconCstAxiom,
IconCstBaseSet,
IconCstConstSet,
@@ -113,6 +114,12 @@ export function RSFormStats({ className, stats }: RSFormStatsProps) {
value={stats.count_theorem}
/>
+ }
+ value={stats.count_crucial}
+ />
LABEL_THRESHOLD ? 'text-[12px]/[16px]' : 'text-[14px]/[20px]'
)}
@@ -50,6 +51,7 @@ export function TGNode(node: TGNodeInternal) {
{description ? (
DESCRIPTION_THRESHOLD ? 'text-[10px]/[12px]' : 'text-[12px]/[16px]'
@@ -69,9 +71,11 @@ export function TGNode(node: TGNodeInternal) {
// ====== INTERNAL ======
function describeCstNode(cst: IConstituenta) {
- return `${cst.alias}: ${cst.term_resolved}
Типизация: ${labelCstTypification(
- cst
- )}
Содержание: ${
- isBasicConcept(cst.cst_type) ? cst.convention : cst.definition_resolved || cst.definition_formal || cst.convention
+ const contents = isBasicConcept(cst.cst_type)
+ ? cst.convention
+ : cst.definition_resolved || cst.definition_formal || cst.convention;
+ const typification = labelCstTypification(cst);
+ return `${cst.alias}: ${cst.term_resolved}
Типизация: ${typification}
Содержание: ${
+ contents ? contents : 'отсутствует'
}`;
}
diff --git a/rsconcept/frontend/src/features/rsform/components/toolbar-graph-selection.tsx b/rsconcept/frontend/src/features/rsform/components/toolbar-graph-selection.tsx
index 5e454480..ed947efb 100644
--- a/rsconcept/frontend/src/features/rsform/components/toolbar-graph-selection.tsx
+++ b/rsconcept/frontend/src/features/rsform/components/toolbar-graph-selection.tsx
@@ -1,6 +1,9 @@
import { MiniButton } from '@/components/control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
import {
+ IconChild,
+ IconContextSelection,
+ IconCrucial,
IconGraphCollapse,
IconGraphCore,
IconGraphExpand,
@@ -8,7 +11,7 @@ import {
IconGraphInverse,
IconGraphMaximize,
IconGraphOutputs,
- IconGraphSelection,
+ IconGroupSelection,
IconPredecessor,
IconReset
} from '@/components/icons';
@@ -21,7 +24,8 @@ interface ToolbarGraphSelectionProps extends Styling {
onChange: (newSelection: number[]) => void;
graph: Graph;
isCore: (item: number) => boolean;
- isOwned?: (item: number) => boolean;
+ isCrucial: (item: number) => boolean;
+ isInherited: (item: number) => boolean;
}
export function ToolbarGraphSelection({
@@ -29,20 +33,66 @@ export function ToolbarGraphSelection({
graph,
value: selected,
isCore,
- isOwned,
+ isInherited,
+ isCrucial,
onChange,
...restProps
}: ToolbarGraphSelectionProps) {
- const menu = useDropdown();
+ const selectedMenu = useDropdown();
+ const groupMenu = useDropdown();
const emptySelection = selected.length === 0;
+ function handleSelectReset() {
+ onChange([]);
+ }
+
function handleSelectCore() {
+ groupMenu.hide();
const core = [...graph.nodes.keys()].filter(isCore);
onChange([...core, ...graph.expandInputs(core)]);
}
function handleSelectOwned() {
- if (isOwned) onChange([...graph.nodes.keys()].filter(isOwned));
+ groupMenu.hide();
+ onChange([...graph.nodes.keys()].filter((item: number) => !isInherited(item)));
+ }
+
+ function handleSelectInherited() {
+ groupMenu.hide();
+ onChange([...graph.nodes.keys()].filter(isInherited));
+ }
+
+ function handleSelectCrucial() {
+ groupMenu.hide();
+ onChange([...graph.nodes.keys()].filter(isCrucial));
+ }
+
+ function handleExpandOutputs() {
+ onChange([...selected, ...graph.expandOutputs(selected)]);
+ }
+
+ function handleExpandInputs() {
+ onChange([...selected, ...graph.expandInputs(selected)]);
+ }
+
+ function handleSelectMaximize() {
+ selectedMenu.hide();
+ onChange(graph.maximizePart(selected));
+ }
+
+ function handleSelectInvert() {
+ selectedMenu.hide();
+ onChange([...graph.nodes.keys()].filter(item => !selected.includes(item)));
+ }
+
+ function handleSelectAllInputs() {
+ selectedMenu.hide();
+ onChange([...graph.expandInputs(selected)]);
+ }
+
+ function handleSelectAllOutputs() {
+ selectedMenu.hide();
+ onChange([...graph.expandOutputs(selected)]);
}
return (
@@ -50,73 +100,99 @@ export function ToolbarGraphSelection({
}
- onClick={() => onChange([])}
+ onClick={handleSelectReset}
disabled={emptySelection}
/>
-
+
+
}
- onClick={menu.toggle}
+ title='Выделить на основе выбранных...'
+ hideTitle={selectedMenu.isOpen}
+ icon={ }
+ onClick={selectedMenu.toggle}
disabled={emptySelection}
/>
-
- }
- onClick={() => onChange([...selected, ...graph.expandAllInputs(selected)])}
- disabled={emptySelection}
- />
- }
- onClick={() => onChange([...selected, ...graph.expandAllOutputs(selected)])}
- disabled={emptySelection}
- />
-
+
}
- onClick={() => onChange([...selected, ...graph.expandInputs(selected)])}
+ onClick={handleExpandInputs}
disabled={emptySelection}
/>
}
- onClick={() => onChange([...selected, ...graph.expandOutputs(selected)])}
+ onClick={handleExpandOutputs}
disabled={emptySelection}
/>
+
+ }
+ onClick={handleSelectAllInputs}
+ disabled={emptySelection}
+ />
+ }
+ onClick={handleSelectAllOutputs}
+ disabled={emptySelection}
+ />
+
}
- onClick={() => onChange(graph.maximizePart(selected))}
+ onClick={handleSelectMaximize}
disabled={emptySelection}
/>
+ }
+ onClick={handleSelectInvert}
+ />
-
}
- onClick={handleSelectCore}
- />
-
}
- onClick={handleSelectOwned}
- />
-
}
- onClick={() => onChange([...graph.nodes.keys()].filter(item => !selected.includes(item)))}
- />
+
+ }
+ onClick={groupMenu.toggle}
+ />
+
+ }
+ onClick={handleSelectCore}
+ />
+ }
+ onClick={handleSelectCrucial}
+ />
+ }
+ onClick={handleSelectOwned}
+ />
+ }
+ onClick={handleSelectInherited}
+ />
+
+
);
}
diff --git a/rsconcept/frontend/src/features/rsform/dialogs/dlg-create-cst/form-create-cst.tsx b/rsconcept/frontend/src/features/rsform/dialogs/dlg-create-cst/form-create-cst.tsx
index 2073c015..b81e6337 100644
--- a/rsconcept/frontend/src/features/rsform/dialogs/dlg-create-cst/form-create-cst.tsx
+++ b/rsconcept/frontend/src/features/rsform/dialogs/dlg-create-cst/form-create-cst.tsx
@@ -6,9 +6,11 @@ import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { HelpTopic } from '@/features/help';
import { BadgeHelp } from '@/features/help/components/badge-help';
+import { MiniButton } from '@/components/control';
import { TextArea, TextInput } from '@/components/input';
import { CstType, type ICreateConstituentaDTO } from '../../backend/types';
+import { IconCrucialValue } from '../../components/icon-crucial-value';
import { RSInput } from '../../components/rs-input';
import { SelectCstType } from '../../components/select-cst-type';
import { getRSDefinitionPlaceholder } from '../../labels';
@@ -30,6 +32,7 @@ export function FormCreateCst({ schema }: FormCreateCstProps) {
const cst_type = useWatch({ control, name: 'cst_type' });
const convention = useWatch({ control, name: 'convention' });
+ const crucial = useWatch({ control, name: 'crucial' });
const isBasic = isBasicConcept(cst_type);
const isElementary = isBaseSet(cst_type);
const isFunction = isFunctional(cst_type);
@@ -41,9 +44,18 @@ export function FormCreateCst({ schema }: FormCreateCstProps) {
setForceComment(false);
}
+ function handleToggleCrucial() {
+ setValue('crucial', !crucial);
+ }
+
return (
<>
+
}
+ onClick={handleToggleCrucial}
+ />
+
}
+ onClick={handleToggleCrucial}
+ />
-
+
{activeCst ? (
state.setIsModified);
const isProcessing = useMutatingRSForm();
- const { updateConstituenta: cstUpdate } = useUpdateConstituenta();
+ const { updateConstituenta } = useUpdateConstituenta();
+ const { updateCrucial } = useUpdateCrucial();
const showTypification = useDialogsStore(state => state.showShowTypeGraph);
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
const showRenameCst = useDialogsStore(state => state.showRenameCst);
@@ -128,7 +133,7 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
}
function onSubmit(data: IUpdateConstituentaDTO) {
- void cstUpdate({ itemID: schema.id, data }).then(() => {
+ void updateConstituenta({ itemID: schema.id, data }).then(() => {
setIsModified(false);
reset({ ...data });
});
@@ -158,33 +163,48 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
showRenameCst({ schema: schema, target: activeCst });
}
- return (
-