From 18ad3f9f293a7e71ba197f8f55f54be1b0664d20 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Tue, 29 Jul 2025 20:55:29 +0300 Subject: [PATCH] F: Implement crucial constituents UI --- .../apps/rsform/serializers/__init__.py | 1 + .../apps/rsform/serializers/data_access.py | 18 ++ .../apps/rsform/tests/s_views/t_rsforms.py | 13 ++ .../backend/apps/rsform/views/rsforms.py | 31 ++++ 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 | 12 +- .../help/items/ui/help-rsgraph-term.tsx | 17 +- .../src/features/rsform/backend/api.ts | 10 ++ .../features/rsform/backend/rsform-loader.ts | 1 + .../src/features/rsform/backend/types.ts | 8 + .../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 | 2 + .../editor-constituenta/form-constituenta.tsx | 33 +++- .../editor-rslist/toolbar-rslist.tsx | 24 +++ .../editor-term-graph/toolbar-term-graph.tsx | 3 +- .../editor-term-graph/view-hidden.tsx | 1 + rsconcept/frontend/src/styling/utilities.css | 4 + 29 files changed, 389 insertions(+), 68 deletions(-) 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/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 a93074a4..e8c04eeb 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -72,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() 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 bd1a983e..6c68f8aa 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -578,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/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 4f1e29a4..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,9 +1,10 @@ import { IconChild, IconClone, + IconContextSelection, + IconCrucial, IconDestroy, IconFilter, - IconGraphSelection, IconKeyboard, IconLeftOpen, IconMoveDown, @@ -26,6 +27,13 @@ export function HelpRSEditor() { return (

Редактор конституенты

+ + +

Команды

@@ -68,7 +76,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/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 29d22a4e..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[]; @@ -360,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} + /> 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); @@ -129,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 }); }); @@ -159,6 +163,16 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, showRenameCst({ schema: schema, target: activeCst }); } + function handleToggleCrucial() { + void updateCrucial({ + itemID: schema.id, + data: { + target: [activeCst.id], + value: !activeCst.crucial + } + }); + } + return (
    + } + onClick={handleToggleCrucial} + disabled={disabled || isProcessing || isModified} + /> + - -
    +
    {activeCst?.alias ?? ''}
    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/styling/utilities.css b/rsconcept/frontend/src/styling/utilities.css index f7ef8dcf..c852080a 100644 --- a/rsconcept/frontend/src/styling/utilities.css +++ b/rsconcept/frontend/src/styling/utilities.css @@ -239,3 +239,7 @@ pointer-events: none; } } + +@utility cc-badge-inner-shadow { + box-shadow: inset 0 1px 3px 0, inset 0 -1px 3px 0; +}