Compare commits

...

12 Commits

Author SHA1 Message Date
Ivan
f919bfe4cf B: Fix substitution UI error
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
2024-12-20 15:01:23 +03:00
Ivan
8bad0890cd F: Improve tooltip performance 2024-12-20 14:36:20 +03:00
Ivan
32d12369c2 M: Improve checkbox structure 2024-12-20 13:36:31 +03:00
Ivan
0de6b17096 M: Prevent animations for darkMode transitions 2024-12-20 12:57:06 +03:00
Ivan
f5b26db1ff F: Improve navigation toggles 2024-12-20 11:54:10 +03:00
Ivan
48ce020c8f F: Improve theme switcher 2024-12-19 18:56:31 +03:00
Ivan
e40fd197b9 M: Use float property instead of overlay 2024-12-18 21:26:44 +03:00
Ivan
48289b2609 npm update 2024-12-18 15:32:46 +03:00
Ivan
1fdfa5012f B: Fix tabs background 2024-12-18 14:54:45 +03:00
Ivan
ab8c6082da M: Fix sidelist dynamics 2024-12-18 14:41:42 +03:00
Ivan
1332dbf088 F: Add QR feature for RSForm 2024-12-18 12:35:17 +03:00
Ivan
26bf15e981 M: Remove schema_id from RSForm items 2024-12-17 22:33:51 +03:00
34 changed files with 532 additions and 435 deletions

View File

@ -42,6 +42,7 @@ This readme file is used mostly to document project dependencies and conventions
- reactflow - reactflow
- js-file-download - js-file-download
- use-debounce - use-debounce
- qrcode.react
- html-to-image - html-to-image
- @tanstack/react-table - @tanstack/react-table
- @uiw/react-codemirror - @uiw/react-codemirror

View File

@ -137,6 +137,7 @@ class RSFormSerializer(serializers.ModelSerializer):
result['inheritance'] = [] result['inheritance'] = []
for cst in RSForm(instance).constituents().defer('order').order_by('order'): for cst in RSForm(instance).constituents().defer('order').order_by('order'):
result['items'].append(CstSerializer(cst).data) result['items'].append(CstSerializer(cst).data)
del result['items'][-1]['schema']
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'): for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
result['oss'].append({ result['oss'].append({
'id': oss.pk, 'id': oss.pk,
@ -178,6 +179,7 @@ class RSFormSerializer(serializers.ModelSerializer):
cst.delete() cst.delete()
else: else:
cst_data = next(x for x in items if x['id'] == cst.pk) cst_data = next(x for x in items if x['id'] == cst.pk)
cst_data['schema'] = cast(LibraryItem, self.instance).pk
new_cst = CstBaseSerializer(data=cst_data) new_cst = CstBaseSerializer(data=cst_data)
new_cst.is_valid(raise_exception=True) new_cst.is_valid(raise_exception=True)
new_cst.validated_data['order'] = ids.index(cst.pk) new_cst.validated_data['order'] = ids.index(cst.pk)
@ -192,6 +194,7 @@ class RSFormSerializer(serializers.ModelSerializer):
cst = schema.insert_new(cst_data['alias']) cst = schema.insert_new(cst_data['alias'])
old_id = cst_data['id'] old_id = cst_data['id']
cst_data['id'] = cst.pk cst_data['id'] = cst.pk
cst_data['schema'] = cast(LibraryItem, self.instance).pk
new_cst = CstBaseSerializer(data=cst_data) new_cst = CstBaseSerializer(data=cst_data)
new_cst.is_valid(raise_exception=True) new_cst.is_valid(raise_exception=True)
new_cst.validated_data['order'] = ids.index(old_id) new_cst.validated_data['order'] = ids.index(old_id)

File diff suppressed because it is too large Load Diff

View File

@ -14,13 +14,14 @@
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@lezer/lr": "^1.4.2", "@lezer/lr": "^1.4.2",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.6",
"@uiw/codemirror-themes": "^4.23.6", "@uiw/codemirror-themes": "^4.23.7",
"@uiw/react-codemirror": "^4.23.6", "@uiw/react-codemirror": "^4.23.7",
"axios": "^1.7.9", "axios": "^1.7.9",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"qrcode.react": "^4.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-error-boundary": "^4.1.2", "react-error-boundary": "^4.1.2",
@ -29,7 +30,7 @@
"react-router": "^7.0.2", "react-router": "^7.0.2",
"react-select": "^5.9.0", "react-select": "^5.9.0",
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",
"react-toastify": "^10.0.6", "react-toastify": "^11.0.0",
"react-tooltip": "^5.28.0", "react-tooltip": "^5.28.0",
"react-zoom-pan-pinch": "^3.6.1", "react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
@ -46,7 +47,7 @@
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"babel-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206", "babel-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
"eslint": "^9.16.0", "eslint": "^9.17.0",
"eslint-plugin-react": "^7.37.2", "eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206", "eslint-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",
@ -54,10 +55,10 @@
"globals": "^15.13.0", "globals": "^15.13.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.16", "tailwindcss": "^3.4.17",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.18.0", "typescript-eslint": "^8.18.1",
"vite": "^6.0.3" "vite": "^6.0.3"
}, },
"overrides": { "overrides": {

View File

@ -10,12 +10,13 @@ import { NavigationState } from '@/context/NavigationContext';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
function ApplicationLayout() { function ApplicationLayout() {
const { viewportHeight, mainHeight, showScroll } = useConceptOptions(); const { viewportHeight, mainHeight, showScroll, noNavigationAnimation } = useConceptOptions();
return ( return (
<NavigationState> <NavigationState>
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'> <div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
<ConceptToaster <ConceptToaster
className='mt-[4rem] text-[14px]' // prettier: split lines className='text-[14px] cc-animate-position'
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
autoClose={3000} autoClose={3000}
draggable={false} draggable={false}
pauseOnFocusLoss={false} pauseOnFocusLoss={false}

View File

@ -31,9 +31,8 @@ function NavigationButton({
className={clsx( className={clsx(
'mr-1 h-full', // prettier: split lines 'mr-1 h-full', // prettier: split lines
'flex items-center gap-1', 'flex items-center gap-1',
'clr-btn-nav cc-animate-color', 'clr-btn-nav cc-animate-color duration-500',
'rounded-xl', 'rounded-xl',
'transition duration-500',
'font-controls whitespace-nowrap', 'font-controls whitespace-nowrap',
{ {
'px-2': text, 'px-2': text,

View File

@ -1,35 +1,53 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { IconPin, IconUnpin } from '@/components/Icons'; import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { globals, PARAMETER } from '@/utils/constants'; import { globals, PARAMETER } from '@/utils/constants';
function ToggleNavigation() { function ToggleNavigation() {
const { noNavigationAnimation, toggleNoNavigation } = useConceptOptions(); const { noNavigationAnimation, noNavigation, toggleNoNavigation, toggleDarkMode, darkMode } = useConceptOptions();
const iconSize = !noNavigationAnimation ? '0.75rem' : '1rem';
return ( return (
<button <div
type='button'
tabIndex={-1}
className={clsx( className={clsx(
'absolute top-0 right-0 z-navigation', 'absolute top-0 right-0 z-navigation',
'min-h-[2rem] min-w-[2rem] sm:min-w-fit', 'min-h-[2rem] min-w-[2rem]',
'flex items-center justify-center', 'flex items-end justify-center gap-1',
'clr-hover', 'select-none',
'select-none' !noNavigation && 'flex-col-reverse'
)} )}
onClick={toggleNoNavigation}
data-tooltip-id={globals.tooltip}
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
style={{ style={{
transitionProperty: 'height, width, background-color', transitionProperty: 'height, width, background-color',
transitionDuration: `${PARAMETER.moveDuration}ms`, transitionDuration: `${PARAMETER.moveDuration}ms`,
height: noNavigationAnimation ? '1.2rem' : '3rem', height: noNavigationAnimation ? '2rem' : '3rem',
width: noNavigationAnimation ? '3rem' : '1.2rem' width: noNavigationAnimation ? '3rem' : '2rem'
}} }}
> >
{!noNavigationAnimation ? <IconPin /> : null} {!noNavigationAnimation ? (
{noNavigationAnimation ? <IconUnpin /> : null} <button
</button> tabIndex={-1}
type='button'
className='p-1'
onClick={toggleDarkMode}
data-tooltip-id={globals.tooltip}
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
>
{darkMode ? <IconDarkTheme size='0.75rem' /> : null}
{!darkMode ? <IconLightTheme size='0.75rem' /> : null}
</button>
) : null}
<button
tabIndex={-1}
type='button'
className='p-1'
onClick={toggleNoNavigation}
data-tooltip-id={globals.tooltip}
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
>
{!noNavigationAnimation ? <IconPin size={iconSize} /> : null}
{noNavigationAnimation ? <IconUnpin size={iconSize} /> : null}
</button>
</div>
); );
} }

View File

@ -20,6 +20,7 @@ export { TbEye as IconShow } from 'react-icons/tb';
export { TbEyeX as IconHide } from 'react-icons/tb'; export { TbEyeX as IconHide } from 'react-icons/tb';
export { BiShareAlt as IconShare } from 'react-icons/bi'; export { BiShareAlt as IconShare } from 'react-icons/bi';
export { LuFilter as IconFilter } from 'react-icons/lu'; export { LuFilter as IconFilter } from 'react-icons/lu';
export { LuQrCode as IconQR } from 'react-icons/lu';
export { LuFilterX as IconFilterReset } from 'react-icons/lu'; export { LuFilterX as IconFilterReset } from 'react-icons/lu';
export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi'; export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
export { LuTriangleAlert as IconAlert } from 'react-icons/lu'; export { LuTriangleAlert as IconAlert } from 'react-icons/lu';

View File

@ -1,10 +1,11 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { CstClass, IConstituenta } from '@/models/rsform'; import { CstClass, IConstituenta } from '@/models/rsform';
import { APP_COLORS, colorFgCstStatus } from '@/styling/color'; import { APP_COLORS, colorFgCstStatus } from '@/styling/color';
import { globals } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
import TooltipConstituenta from './TooltipConstituenta';
interface BadgeConstituentaProps extends CProps.Styling { interface BadgeConstituentaProps extends CProps.Styling {
/** Prefix for tooltip ID. */ /** Prefix for tooltip ID. */
@ -18,6 +19,8 @@ interface BadgeConstituentaProps extends CProps.Styling {
* Displays a badge with a constituenta alias and information tooltip. * Displays a badge with a constituenta alias and information tooltip.
*/ */
function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstituentaProps) { function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstituentaProps) {
const { setHoverCst } = useConceptOptions();
return ( return (
<div <div
id={`${prefixID}${value.id}`} id={`${prefixID}${value.id}`}
@ -35,9 +38,10 @@ function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstitue
backgroundColor: value.cst_class === CstClass.BASIC ? APP_COLORS.bgGreen25 : APP_COLORS.bgInput, backgroundColor: value.cst_class === CstClass.BASIC ? APP_COLORS.bgGreen25 : APP_COLORS.bgInput,
...style ...style
}} }}
data-tooltip-id={globals.constituenta_tooltip}
onMouseEnter={() => setHoverCst(value)}
> >
{value.alias} {value.alias}
<TooltipConstituenta anchor={`#${prefixID}${value.id}`} data={value} />
</div> </div>
); );
} }

View File

@ -1,18 +0,0 @@
import InfoConstituenta from '@/components/info/InfoConstituenta';
import Tooltip from '@/components/ui/Tooltip';
import { IConstituenta } from '@/models/rsform';
interface TooltipConstituentaProps {
data: IConstituenta;
anchor: string;
}
function TooltipConstituenta({ data, anchor }: TooltipConstituentaProps) {
return (
<Tooltip clickable layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[30rem]'>
<InfoConstituenta data={data} onClick={event => event.stopPropagation()} />
</Tooltip>
);
}
export default TooltipConstituenta;

View File

@ -65,7 +65,8 @@ function Checkbox({
<div <div
className={clsx( className={clsx(
'max-w-[1rem] min-w-[1rem] h-4', // prettier: split lines 'max-w-[1rem] min-w-[1rem] h-4', // prettier: split lines
'border rounded-sm ', 'pt-[0.1rem] pl-[0.1rem]',
'border rounded-sm',
'cc-animate-color', 'cc-animate-color',
{ {
'bg-sec-600 text-sec-0': value !== false, 'bg-sec-600 text-sec-0': value !== false,
@ -73,11 +74,7 @@ function Checkbox({
} }
)} )}
> >
{value ? ( {value ? <CheckboxChecked /> : null}
<div className='mt-[1px] ml-[1px]'>
<CheckboxChecked />
</div>
) : null}
</div> </div>
{label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null} {label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null}
</button> </button>

View File

@ -66,6 +66,7 @@ function CheckboxTristate({
<div <div
className={clsx( className={clsx(
'w-4 h-4', // prettier: split lines 'w-4 h-4', // prettier: split lines
'pt-[0.1rem] pl-[0.1rem]',
'border rounded-sm', 'border rounded-sm',
'cc-animate-color', 'cc-animate-color',
{ {
@ -74,16 +75,8 @@ function CheckboxTristate({
} }
)} )}
> >
{value ? ( {value ? <CheckboxChecked /> : null}
<div className='mt-[1px] ml-[1px]'> {value == null ? <CheckboxNull /> : null}
<CheckboxChecked />
</div>
) : null}
{value == null ? (
<div className='mt-[1px] ml-[1px]'>
<CheckboxNull />
</div>
) : null}
</div> </div>
{label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null} {label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null}
</button> </button>

View File

@ -12,7 +12,6 @@ import BadgeHelp from '../info/BadgeHelp';
import { CProps } from '../props'; import { CProps } from '../props';
import Button from './Button'; import Button from './Button';
import MiniButton from './MiniButton'; import MiniButton from './MiniButton';
import Overlay from './Overlay';
export interface ModalProps extends CProps.Styling { export interface ModalProps extends CProps.Styling {
/** Title of the modal window. */ /** Title of the modal window. */
@ -105,20 +104,20 @@ function Modal({
'border rounded-xl bg-prim-100' 'border rounded-xl bg-prim-100'
)} )}
> >
<Overlay position='right-2 top-2'>
<MiniButton
noPadding
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />}
onClick={handleCancel}
/>
</Overlay>
{helpTopic && !hideHelpWhen?.() ? ( {helpTopic && !hideHelpWhen?.() ? (
<Overlay position='left-2 top-2'> <div className='float-left mt-2 ml-2'>
<BadgeHelp topic={helpTopic} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} padding='p-0' /> <BadgeHelp topic={helpTopic} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} padding='p-0' />
</Overlay> </div>
) : null} ) : null}
<MiniButton
noPadding
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />}
className='float-right mt-2 mr-2'
onClick={handleCancel}
/>
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null} {header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
<div <div

View File

@ -20,7 +20,7 @@ function TabLabel({ label, title, titleHtml, hideTitle, className, ...otherProps
className={clsx( className={clsx(
'min-w-[5.5rem] h-full', 'min-w-[5.5rem] h-full',
'px-2 py-1 flex justify-center', 'px-2 py-1 flex justify-center',
'clr-hover bg-prim-200 cc-animate-color', 'clr-hover cc-animate-color duration-150',
'text-sm whitespace-nowrap font-controls', 'text-sm whitespace-nowrap font-controls',
'select-none hover:cursor-pointer', 'select-none hover:cursor-pointer',
'outline-none', 'outline-none',

View File

@ -1,9 +1,13 @@
'use client'; 'use client';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { flushSync } from 'react-dom';
import InfoConstituenta from '@/components/info/InfoConstituenta';
import Loader from '@/components/ui/Loader';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { IConstituenta } from '@/models/rsform';
import { globals, PARAMETER, storage } from '@/utils/constants'; import { globals, PARAMETER, storage } from '@/utils/constants';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
@ -36,6 +40,8 @@ interface IOptionsContext {
location: string; location: string;
setLocation: React.Dispatch<React.SetStateAction<string>>; setLocation: React.Dispatch<React.SetStateAction<string>>;
setHoverCst: (newValue: IConstituenta | undefined) => void;
calculateHeight: (offset: string, minimum?: string) => string; calculateHeight: (offset: string, minimum?: string) => string;
} }
@ -61,6 +67,8 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
const [noFooter, setNoFooter] = useState(false); const [noFooter, setNoFooter] = useState(false);
const [showScroll, setShowScroll] = useState(false); const [showScroll, setShowScroll] = useState(false);
const [hoverCst, setHoverCst] = useState<IConstituenta | undefined>(undefined);
function setDarkClass(isDark: boolean) { function setDarkClass(isDark: boolean) {
const root = window.document.documentElement; const root = window.document.documentElement;
if (isDark) { if (isDark) {
@ -99,7 +107,26 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
); );
const toggleDarkMode = useCallback(() => { const toggleDarkMode = useCallback(() => {
setDarkMode(prev => !prev); if (!document.startViewTransition) {
setDarkMode(prev => !prev);
} else {
const style = document.createElement('style');
style.innerHTML = `
* {
animation: none !important;
transition: none !important;
}
`;
document.head.appendChild(style);
document.startViewTransition(() => {
flushSync(() => {
setDarkMode(prev => !prev);
});
});
setTimeout(() => document.head.removeChild(style), PARAMETER.moveDuration);
}
}, [setDarkMode]); }, [setDarkMode]);
const mainHeight = useMemo(() => { const mainHeight = useMemo(() => {
@ -138,23 +165,27 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
toggleShowHelp: () => setShowHelp(prev => !prev), toggleShowHelp: () => setShowHelp(prev => !prev),
viewportHeight, viewportHeight,
mainHeight, mainHeight,
calculateHeight calculateHeight,
setHoverCst
}} }}
> >
<> <>
<Tooltip <Tooltip
float // prettier: split-lines float
id={`${globals.tooltip}`} id={globals.tooltip}
layer='z-topmost' layer='z-topmost'
place='right-start' place='right-start'
className='mt-8 max-w-[20rem] break-words' className='mt-8 max-w-[20rem] break-words'
/> />
<Tooltip <Tooltip
float float
id={`${globals.value_tooltip}`} id={globals.value_tooltip}
layer='z-topmost' layer='z-topmost'
className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify' className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify'
/> />
<Tooltip clickable id={globals.constituenta_tooltip} layer='z-modalTooltip' className='max-w-[30rem]'>
{hoverCst ? <InfoConstituenta data={hoverCst} onClick={event => event.stopPropagation()} /> : <Loader />}
</Tooltip>
{children} {children}
</> </>

View File

@ -140,7 +140,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={setActiveTab} onSelect={setActiveTab}
> >
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}> <TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none', 'bg-prim-200')}>
<TabLabel label='Шаблон' title='Выбор шаблона выражения' className='w-[8rem]' /> <TabLabel label='Шаблон' title='Выбор шаблона выражения' className='w-[8rem]' />
<TabLabel label='Аргументы' title='Подстановка аргументов шаблона' className='w-[8rem]' /> <TabLabel label='Аргументы' title='Подстановка аргументов шаблона' className='w-[8rem]' />
<TabLabel label='Конституента' title='Редактирование конституенты' className='w-[8rem]' /> <TabLabel label='Конституента' title='Редактирование конституенты' className='w-[8rem]' />

View File

@ -110,7 +110,9 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={handleSelectTab} onSelect={handleSelectTab}
> >
<TabList className={clsx('self-center absolute top-[2.4rem]', 'flex', 'border divide-x rounded-none')}> <TabList
className={clsx('self-center absolute top-[2.4rem]', 'flex', 'border divide-x rounded-none', 'bg-prim-200')}
>
<TabLabel <TabLabel
title={describeOperationType(OperationType.INPUT)} title={describeOperationType(OperationType.INPUT)}
label={labelOperationType(OperationType.INPUT)} label={labelOperationType(OperationType.INPUT)}

View File

@ -95,7 +95,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
}, [schemasIDs, needPreload, cache]); }, [schemasIDs, needPreload, cache]);
useEffect(() => { useEffect(() => {
if (cache.loading || schemas.length !== schemasIDs.length) { if (cache.loading || schemas.length !== schemasIDs.length || schemas.length === 0) {
return; return;
} }
setSubstitutions(prev => setSubstitutions(prev =>
@ -114,7 +114,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
}, [schemasIDs, schemas, cache.loading, getSchemaByCst]); }, [schemasIDs, schemas, cache.loading, getSchemaByCst]);
useEffect(() => { useEffect(() => {
if (cache.loading || schemas.length !== schemasIDs.length) { if (cache.loading || schemas.length !== schemasIDs.length || schemas.length === 0) {
return; return;
} }
const validator = new SubstitutionValidator(schemas, substitutions); const validator = new SubstitutionValidator(schemas, substitutions);
@ -155,7 +155,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={setActiveTab} onSelect={setActiveTab}
> >
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}> <TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none', 'bg-prim-200')}>
<TabLabel title='Текстовые поля' label='Карточка' className='w-[8rem]' /> <TabLabel title='Текстовые поля' label='Карточка' className='w-[8rem]' />
{target.operation_type === OperationType.SYNTHESIS ? ( {target.operation_type === OperationType.SYNTHESIS ? (
<TabLabel title='Выбор аргументов операции' label='Аргументы' className='w-[8rem]' /> <TabLabel title='Выбор аргументов операции' label='Аргументы' className='w-[8rem]' />

View File

@ -44,7 +44,7 @@ function TabSynthesis({
disabled disabled
value={validationText} value={validationText}
rows={4} rows={4}
style={{ borderColor: isCorrect ? undefined : APP_COLORS.fgRed }} style={{ borderColor: isCorrect ? undefined : APP_COLORS.fgRed, borderWidth: isCorrect ? undefined : '2px' }}
/> />
</div> </div>
</DataLoader> </DataLoader>

View File

@ -55,7 +55,7 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={setActiveTab} onSelect={setActiveTab}
> >
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}> <TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none', 'bg-prim-200')}>
<TabLabel title='Отсылка на термин в заданной словоформе' label={labelReferenceType(ReferenceType.ENTITY)} /> <TabLabel title='Отсылка на термин в заданной словоформе' label={labelReferenceType(ReferenceType.ENTITY)} />
<TabLabel <TabLabel
title='Установление синтаксической связи с отсылкой на термин' title='Установление синтаксической связи с отсылкой на термин'

View File

@ -70,7 +70,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={setActiveTab} onSelect={setActiveTab}
> >
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}> <TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none', 'bg-prim-200')}>
<TabLabel label='Схема' title='Источник конституент' className='w-[8rem]' /> <TabLabel label='Схема' title='Источник конституент' className='w-[8rem]' />
<TabLabel label='Содержание' title='Перечень конституент' className='w-[8rem]' /> <TabLabel label='Содержание' title='Перечень конституент' className='w-[8rem]' />
<TabLabel label='Отождествления' title='Таблица отождествлений' className='w-[8rem]' /> <TabLabel label='Отождествления' title='Таблица отождествлений' className='w-[8rem]' />

View File

@ -0,0 +1,26 @@
'use client';
import clsx from 'clsx';
import { QRCodeSVG } from 'qrcode.react';
import Modal, { ModalProps } from '@/components/ui/Modal';
interface DlgShowQRProps extends Pick<ModalProps, 'hideWindow'> {
target: string;
}
function DlgShowQR({ hideWindow, target }: DlgShowQRProps) {
return (
<Modal
readonly
hideWindow={hideWindow}
className={clsx('w-[30rem]', 'py-12 pr-3 pl-6 flex gap-3 justify-center items-center')}
>
<div className='bg-[#ffffff] p-4 border'>
<QRCodeSVG value={target} size={256} />
</div>
</Modal>
);
}
export default DlgShowQR;

View File

@ -75,6 +75,7 @@ export class RSFormLoader {
const order = this.graph.topologicalOrder(); const order = this.graph.topologicalOrder();
order.forEach(cstID => { order.forEach(cstID => {
const cst = this.cstByID.get(cstID)!; const cst = this.cstByID.get(cstID)!;
cst.schema = this.schema.id;
cst.status = inferStatus(cst.parse.status, cst.parse.valueClass); cst.status = inferStatus(cst.parse.status, cst.parse.valueClass);
cst.is_template = inferTemplate(cst.definition_formal); cst.is_template = inferTemplate(cst.definition_formal);
cst.cst_class = inferClass(cst.cst_type, cst.is_template); cst.cst_class = inferClass(cst.cst_type, cst.is_template);

View File

@ -70,9 +70,10 @@ export class SubstitutionValidator {
constructor(schemas: IRSForm[], substitutions: ICstSubstitute[]) { constructor(schemas: IRSForm[], substitutions: ICstSubstitute[]) {
this.schemas = schemas; this.schemas = schemas;
this.substitutions = substitutions; this.substitutions = substitutions;
if (this.substitutions.length === 0) { if (schemas.length === 0 || substitutions.length === 0) {
return; return;
} }
schemas.forEach(schema => { schemas.forEach(schema => {
this.schemaByID.set(schema.id, schema); this.schemaByID.set(schema.id, schema);
this.mapping.set(schema.id, {}); this.mapping.set(schema.id, {});

View File

@ -65,7 +65,6 @@ export interface TermForm {
*/ */
export interface IConstituentaMeta { export interface IConstituentaMeta {
id: ConstituentaID; id: ConstituentaID;
schema: LibraryItemID;
alias: string; alias: string;
convention: string; convention: string;
cst_type: CstType; cst_type: CstType;
@ -101,6 +100,9 @@ export interface IConstituentaData extends IConstituentaMeta {
* Represents Constituenta. * Represents Constituenta.
*/ */
export interface IConstituenta extends IConstituentaData { export interface IConstituenta extends IConstituentaData {
/** {@link LibraryItemID} of this {@link IConstituenta}. */
schema: LibraryItemID;
/** {@link CstClass} of this {@link IConstituenta}. */ /** {@link CstClass} of this {@link IConstituenta}. */
cst_class: CstClass; cst_class: CstClass;
/** {@link ExpressionStatus} of this {@link IConstituenta}. */ /** {@link ExpressionStatus} of this {@link IConstituenta}. */
@ -135,7 +137,7 @@ export interface IConstituenta extends IConstituentaData {
/** /**
* Represents {@link IConstituenta} reference. * Represents {@link IConstituenta} reference.
*/ */
export interface IConstituentaReference extends Pick<IConstituentaMeta, 'id' | 'schema'> {} export interface IConstituentaReference extends Pick<IConstituenta, 'id' | 'schema'> {}
/** /**
* Represents Constituenta list. * Represents Constituenta list.

View File

@ -117,7 +117,7 @@ function OssTabs() {
className='flex flex-col mx-auto min-w-fit' className='flex flex-col mx-auto min-w-fit'
> >
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'> <Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
<TabList className={clsx('w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2')}> <TabList className={clsx('w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}>
<MenuOssTabs onDestroy={onDestroySchema} /> <MenuOssTabs onDestroy={onDestroySchema} />
<TabLabel label='Карточка' title={schema.title ?? ''} /> <TabLabel label='Карточка' title={schema.title ?? ''} />

View File

@ -15,7 +15,7 @@ import FormConstituenta from './FormConstituenta';
import ToolbarConstituenta from './ToolbarConstituenta'; import ToolbarConstituenta from './ToolbarConstituenta';
// Threshold window width to switch layout. // Threshold window width to switch layout.
const SIDELIST_LAYOUT_THRESHOLD = 1000; // px const SIDELIST_LAYOUT_THRESHOLD = 1050; // px
interface EditorConstituentaProps { interface EditorConstituentaProps {
activeCst?: IConstituenta; activeCst?: IConstituenta;
@ -87,7 +87,7 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
tabIndex={-1} tabIndex={-1}
className={clsx( className={clsx(
'cc-fade-in', 'cc-fade-in',
'min-h-[20rem] max-w-[95rem] mx-auto', 'min-h-[20rem] max-w-[calc(min(100vw,95rem))] mx-auto',
'flex pt-[1.9rem]', 'flex pt-[1.9rem]',
'overflow-y-auto overflow-x-clip', 'overflow-y-auto overflow-x-clip',
{ 'flex-col md:items-center': isNarrow } { 'flex-col md:items-center': isNarrow }

View File

@ -3,7 +3,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { IconDropArrow, IconDropArrowUp } from '@/components/Icons'; import { IconDropArrow, IconDropArrowUp } from '@/components/Icons';
import TooltipConstituenta from '@/components/info/TooltipConstituenta';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
@ -13,7 +12,7 @@ import useWindowSize from '@/hooks/useWindowSize';
import { GraphColoring } from '@/models/miscellaneous'; import { GraphColoring } from '@/models/miscellaneous';
import { ConstituentaID, IRSForm } from '@/models/rsform'; import { ConstituentaID, IRSForm } from '@/models/rsform';
import { APP_COLORS, colorBgGraphNode } from '@/styling/color'; import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
import { PARAMETER, prefixes, storage } from '@/utils/constants'; import { globals, PARAMETER, prefixes, storage } from '@/utils/constants';
interface ViewHiddenProps { interface ViewHiddenProps {
items: ConstituentaID[]; items: ConstituentaID[];
@ -31,6 +30,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const localSelected = items.filter(id => selected.includes(id)); const localSelected = items.filter(id => selected.includes(id));
const [isFolded, setIsFolded] = useLocalStorage(storage.rsgraphFoldHidden, false); const [isFolded, setIsFolded] = useLocalStorage(storage.rsgraphFoldHidden, false);
const { setHoverCst } = useConceptOptions();
function handleClick(cstID: ConstituentaID, event: CProps.EventMouse) { function handleClick(cstID: ConstituentaID, event: CProps.EventMouse) {
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
@ -89,29 +89,27 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
const adjustedColoring = coloringScheme === 'none' ? 'status' : coloringScheme; const adjustedColoring = coloringScheme === 'none' ? 'status' : coloringScheme;
const id = `${prefixes.cst_hidden_list}${cst.alias}`; const id = `${prefixes.cst_hidden_list}${cst.alias}`;
return ( return (
<div key={`wrap-${id}`}> <button
<button key={id}
type='button' type='button'
key={id} className='min-w-[3rem] rounded-md text-center select-none'
id={id} style={{
className='min-w-[3rem] rounded-md text-center select-none' backgroundColor: colorBgGraphNode(cst, adjustedColoring),
style={{ ...(localSelected.includes(cstID)
backgroundColor: colorBgGraphNode(cst, adjustedColoring), ? {
...(localSelected.includes(cstID) outlineWidth: '2px',
? { outlineStyle: cst.is_inherited ? 'dashed' : 'solid',
outlineWidth: '2px', outlineColor: APP_COLORS.fgDefault
outlineStyle: cst.is_inherited ? 'dashed' : 'solid', }
outlineColor: APP_COLORS.fgDefault : {})
} }}
: {}) onClick={event => handleClick(cstID, event)}
}} onDoubleClick={() => onEdit(cstID)}
onClick={event => handleClick(cstID, event)} data-tooltip-id={globals.constituenta_tooltip}
onDoubleClick={() => onEdit(cstID)} onMouseEnter={() => setHoverCst(cst)}
> >
{cst.alias} {cst.alias}
</button> </button>
<TooltipConstituenta data={cst} anchor={`#${id}`} />
</div>
); );
})} })}
</div> </div>

View File

@ -19,6 +19,7 @@ import {
IconNewVersion, IconNewVersion,
IconOSS, IconOSS,
IconOwner, IconOwner,
IconQR,
IconReader, IconReader,
IconReplace, IconReplace,
IconShare, IconShare,
@ -85,6 +86,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
controller.share(); controller.share();
} }
function handleShowQR() {
schemaMenu.hide();
controller.showQR();
}
function handleCreateVersion() { function handleCreateVersion() {
schemaMenu.hide(); schemaMenu.hide();
controller.createVersion(); controller.createVersion();
@ -155,10 +161,16 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleShare} onClick={handleShare}
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC} disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
/> />
<DropdownButton
text='QR-код'
title='Показать QR-код схемы'
icon={<IconQR size='1rem' className='icon-primary' />}
onClick={handleShowQR}
/>
{user ? ( {user ? (
<DropdownButton <DropdownButton
text='Клонировать' text='Клонировать'
icon={<IconClone size='1rem' className='icon-primary' />} icon={<IconClone size='1rem' className='icon-green' />}
disabled={model.isArchive} disabled={model.isArchive}
onClick={handleClone} onClick={handleClone}
/> />

View File

@ -21,6 +21,7 @@ import DlgEditVersions from '@/dialogs/DlgEditVersions';
import DlgEditWordForms from '@/dialogs/DlgEditWordForms'; import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
import DlgInlineSynthesis from '@/dialogs/DlgInlineSynthesis'; import DlgInlineSynthesis from '@/dialogs/DlgInlineSynthesis';
import DlgRenameCst from '@/dialogs/DlgRenameCst'; import DlgRenameCst from '@/dialogs/DlgRenameCst';
import DlgShowQR from '@/dialogs/DlgShowQR';
import DlgShowTypeGraph from '@/dialogs/DlgShowTypeGraph'; import DlgShowTypeGraph from '@/dialogs/DlgShowTypeGraph';
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst'; import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
@ -108,6 +109,7 @@ export interface IRSEditContext extends ILibraryItemEditor {
substitute: () => void; substitute: () => void;
showTypeGraph: () => void; showTypeGraph: () => void;
showQR: () => void;
} }
const RSEditContext = createContext<IRSEditContext | null>(null); const RSEditContext = createContext<IRSEditContext | null>(null);
@ -172,6 +174,7 @@ export const RSEditState = ({
const [showEditVersions, setShowEditVersions] = useState(false); const [showEditVersions, setShowEditVersions] = useState(false);
const [showInlineSynthesis, setShowInlineSynthesis] = useState(false); const [showInlineSynthesis, setShowInlineSynthesis] = useState(false);
const [showTypeGraph, setShowTypeGraph] = useState(false); const [showTypeGraph, setShowTypeGraph] = useState(false);
const [showQR, setShowQR] = useState(false);
const [createInitialData, setCreateInitialData] = useState<ICstCreateData>(); const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
const [showCreateCst, setShowCreateCst] = useState(false); const [showCreateCst, setShowCreateCst] = useState(false);
@ -627,6 +630,11 @@ export const RSEditState = ({
[model] [model]
); );
function generateQR(): string {
const currentRef = window.location.href;
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
}
return ( return (
<RSEditContext <RSEditContext
value={{ value={{
@ -679,11 +687,13 @@ export const RSEditState = ({
produceStructure, produceStructure,
substitute, substitute,
showTypeGraph: () => setShowTypeGraph(true) showTypeGraph: () => setShowTypeGraph(true),
showQR: () => setShowQR(true)
}} }}
> >
{model.schema ? ( {model.schema ? (
<> <>
{showQR ? <DlgShowQR hideWindow={() => setShowQR(false)} target={generateQR()} /> : null}
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null} {showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
{showClone ? ( {showClone ? (
<DlgCloneLibraryItem <DlgCloneLibraryItem

View File

@ -197,7 +197,9 @@ function RSTabs() {
className='flex flex-col mx-auto min-w-fit' className='flex flex-col mx-auto min-w-fit'
> >
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'> <Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
<TabList className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2')}> <TabList
className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}
>
<MenuRSTabs onDestroy={onDestroySchema} /> <MenuRSTabs onDestroy={onDestroySchema} />
<TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} /> <TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} />

View File

@ -63,7 +63,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
<SearchBar <SearchBar
id='constituents_search' id='constituents_search'
noBorder noBorder
className='min-w-[6rem] pr-2 flex-grow' className='min-w-[6rem] w-[6rem] mr-2 flex-grow'
query={filterText} query={filterText}
onChangeQuery={setFilterText} onChangeQuery={setFilterText}
/> />

View File

@ -2,5 +2,4 @@
* Module: List external styling imports. * Module: List external styling imports.
*/ */
@import 'react-toastify/dist/ReactToastify.css';
@import 'reactflow/dist/style.css'; @import 'reactflow/dist/style.css';

View File

@ -144,6 +144,7 @@ export const storage = {
export const globals = { export const globals = {
tooltip: 'global_tooltip', tooltip: 'global_tooltip',
value_tooltip: 'value_tooltip', value_tooltip: 'value_tooltip',
constituenta_tooltip: 'cst_tooltip',
password_tooltip: 'password_tooltip', password_tooltip: 'password_tooltip',
email_tooltip: 'email_tooltip', email_tooltip: 'email_tooltip',
main_scroll: 'main_scroll', main_scroll: 'main_scroll',