From ba0416c37d1647dd7388c528fdc4d66aed9ef223 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Tue, 29 Jul 2025 20:56:31 +0300 Subject: [PATCH] F: Implement crucial constituents feature --- .../apps/library/tests/s_views/t_versions.py | 5 +- .../oss/tests/s_propagation/t_constituents.py | 5 +- rsconcept/backend/apps/rsform/admin.py | 2 +- .../migrations/0004_constituenta_crucial.py | 18 ++ .../apps/rsform/models/Constituenta.py | 5 + .../backend/apps/rsform/models/RSForm.py | 4 + .../apps/rsform/serializers/__init__.py | 1 + .../apps/rsform/serializers/data_access.py | 25 ++- .../apps/rsform/tests/s_views/t_rsforms.py | 19 +- .../backend/apps/rsform/views/rsforms.py | 31 ++++ .../src/components/control/text-button.tsx | 37 ++++ .../src/components/control/text-url.tsx | 2 +- rsconcept/frontend/src/components/icons.tsx | 6 +- .../src/features/ai/models/prompting-api.ts | 10 +- .../features/help/items/help-thesaurus.tsx | 6 + .../features/help/items/ui/help-rseditor.tsx | 16 +- .../help/items/ui/help-rsgraph-term.tsx | 17 +- .../side-panel/toolbar-schema.tsx | 2 + .../src/features/rsform/backend/api.ts | 10 ++ .../features/rsform/backend/rsform-loader.ts | 1 + .../src/features/rsform/backend/types.ts | 13 +- .../rsform/backend/use-update-crucial.ts | 25 +++ .../rsform/components/badge-constituenta.tsx | 1 + .../rsform/components/icon-crucial-value.tsx | 10 ++ .../components/icon-dependency-mode.tsx | 6 +- .../rsform/components/info-constituenta.tsx | 3 +- .../components/pick-multi-constituenta.tsx | 3 +- .../rsform/components/rsform-stats.tsx | 7 + .../components/term-graph/graph/tg-node.tsx | 12 +- .../components/toolbar-graph-selection.tsx | 166 +++++++++++++----- .../dlg-create-cst/form-create-cst.tsx | 12 ++ .../dialogs/dlg-edit-cst/form-edit-cst.tsx | 12 ++ .../src/features/rsform/models/rsform.ts | 3 + .../editor-constituenta.tsx | 2 +- .../editor-constituenta/form-constituenta.tsx | 88 ++++++---- .../editor-rsexpression/status-bar.tsx | 8 +- .../editor-rslist/toolbar-rslist.tsx | 24 +++ .../editor-term-graph/toolbar-term-graph.tsx | 3 +- .../editor-term-graph/view-hidden.tsx | 1 + .../rsform/pages/rsform-page/rsedit-state.tsx | 2 + rsconcept/frontend/src/styling/utilities.css | 10 ++ 41 files changed, 518 insertions(+), 115 deletions(-) create mode 100644 rsconcept/backend/apps/rsform/migrations/0004_constituenta_crucial.py create mode 100644 rsconcept/frontend/src/components/control/text-button.tsx create mode 100644 rsconcept/frontend/src/features/rsform/backend/use-update-crucial.ts create mode 100644 rsconcept/frontend/src/features/rsform/components/icon-crucial-value.tsx 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 ( + + ); +} 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 ( - ); 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 ( -
    void handleSubmit(onSubmit)(event)}> - {!disabled || isProcessing ? ( - } - disabled={isModified} - /> - ) : null} + function handleToggleCrucial() { + void updateCrucial({ + itemID: schema.id, + data: { + target: [activeCst.id], + value: !activeCst.crucial + } + }); + } -
    -
    - Имя - {activeCst?.alias ?? ''} + return ( + void handleSubmit(onSubmit)(event)} + > +
    + + + } + onClick={handleToggleCrucial} + disabled={disabled || isProcessing || isModified} + /> + + +
    + {activeCst?.alias ?? ''}
    - {!disabled || isProcessing ? ( - } - disabled={isModified} - /> - ) : null}
    ( setForceComment(true)} - > - Добавить комментарий - + /> ) : null} {!disabled || isProcessing ? ( diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rsexpression/status-bar.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rsexpression/status-bar.tsx index 1d862338..48cc320d 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rsexpression/status-bar.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rsexpression/status-bar.tsx @@ -41,11 +41,11 @@ export function StatusBar({ className, isModified, processing, activeCst, parseD })(); return ( -
    +
    ) : null} {!processing ? ( -
    +
    - {labelExpressionStatus(status)} + {labelExpressionStatus(status)}
    ) : null}
    diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rslist/toolbar-rslist.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rslist/toolbar-rslist.tsx index 34a19e91..3ef5ad91 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rslist/toolbar-rslist.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rslist/toolbar-rslist.tsx @@ -3,11 +3,13 @@ import { HelpTopic } from '@/features/help'; import { BadgeHelp } from '@/features/help/components/badge-help'; import { MiniSelectorOSS } from '@/features/library/components/mini-selector-oss'; +import { useUpdateCrucial } from '@/features/rsform/backend/use-update-crucial'; import { MiniButton } from '@/components/control'; import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown'; import { IconClone, + IconCrucial, IconDestroy, IconMoveDown, IconMoveUp, @@ -31,10 +33,12 @@ interface ToolbarRSListProps { export function ToolbarRSList({ className }: ToolbarRSListProps) { const isProcessing = useMutatingRSForm(); + const { updateCrucial } = useUpdateCrucial(); const menu = useDropdown(); const { schema, selected, + activeCst, navigateOss, deselectAll, createCst, @@ -46,6 +50,19 @@ export function ToolbarRSList({ className }: ToolbarRSListProps) { moveDown } = useRSEdit(); + function handleToggleCrucial() { + if (!activeCst) { + return; + } + void updateCrucial({ + itemID: schema.id, + data: { + target: selected, + value: !activeCst.crucial + } + }); + } + return (
    {schema.oss.length > 0 ? ( @@ -75,6 +92,13 @@ export function ToolbarRSList({ className }: ToolbarRSListProps) { onClick={moveDown} disabled={isProcessing || selected.length === 0 || selected.length === schema.items.length} /> + } + onClick={handleToggleCrucial} + disabled={isProcessing || selected.length === 0} + />
    0 ? cstID => !schema.cstByID.get(cstID)?.is_inherited : undefined} + isCrucial={cstID => schema.cstByID.get(cstID)?.crucial ?? false} + isInherited={cstID => schema.cstByID.get(cstID)?.is_inherited ?? false} value={selected} onChange={handleSetSelected} /> diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/view-hidden.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/view-hidden.tsx index e2df7f81..1c6278dd 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/view-hidden.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/view-hidden.tsx @@ -80,6 +80,7 @@ export function ViewHidden({ items }: ViewHiddenProps) { type='button' className={clsx( 'cc-view-hidden-item w-12 rounded-md text-center select-none', + cst.crucial && 'text-primary', localSelected.includes(cstID) && 'selected', cst.is_inherited && 'inherited' )} diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/rsedit-state.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/rsedit-state.tsx index dc211817..5ed75dc0 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/rsedit-state.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/rsedit-state.tsx @@ -225,6 +225,7 @@ export const RSEditState = ({ definition_formal: definition ?? '', definition_raw: '', convention: '', + crucial: false, term_forms: [] }; if (skipDialog) { @@ -248,6 +249,7 @@ export const RSEditState = ({ definition_formal: activeCst.definition_formal, definition_raw: activeCst.definition_raw, convention: activeCst.convention, + crucial: activeCst.crucial, term_forms: activeCst.term_forms } }).then(onCreateCst); diff --git a/rsconcept/frontend/src/styling/utilities.css b/rsconcept/frontend/src/styling/utilities.css index 06e887fc..c852080a 100644 --- a/rsconcept/frontend/src/styling/utilities.css +++ b/rsconcept/frontend/src/styling/utilities.css @@ -40,6 +40,12 @@ } } +@utility cc-hover-underline { + &:hover:not(:disabled) { + text-decoration: underline; + } +} + @utility focus-outline { --focus-color: var(--color-ring); @@ -233,3 +239,7 @@ pointer-events: none; } } + +@utility cc-badge-inner-shadow { + box-shadow: inset 0 1px 3px 0, inset 0 -1px 3px 0; +}