Compare commits
12 Commits
939652cef0
...
f919bfe4cf
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f919bfe4cf | ||
![]() |
8bad0890cd | ||
![]() |
32d12369c2 | ||
![]() |
0de6b17096 | ||
![]() |
f5b26db1ff | ||
![]() |
48ce020c8f | ||
![]() |
e40fd197b9 | ||
![]() |
48289b2609 | ||
![]() |
1fdfa5012f | ||
![]() |
ab8c6082da | ||
![]() |
1332dbf088 | ||
![]() |
26bf15e981 |
|
@ -42,6 +42,7 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
- reactflow
|
||||
- js-file-download
|
||||
- use-debounce
|
||||
- qrcode.react
|
||||
- html-to-image
|
||||
- @tanstack/react-table
|
||||
- @uiw/react-codemirror
|
||||
|
|
|
@ -137,6 +137,7 @@ class RSFormSerializer(serializers.ModelSerializer):
|
|||
result['inheritance'] = []
|
||||
for cst in RSForm(instance).constituents().defer('order').order_by('order'):
|
||||
result['items'].append(CstSerializer(cst).data)
|
||||
del result['items'][-1]['schema']
|
||||
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
|
||||
result['oss'].append({
|
||||
'id': oss.pk,
|
||||
|
@ -178,6 +179,7 @@ class RSFormSerializer(serializers.ModelSerializer):
|
|||
cst.delete()
|
||||
else:
|
||||
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.is_valid(raise_exception=True)
|
||||
new_cst.validated_data['order'] = ids.index(cst.pk)
|
||||
|
@ -192,6 +194,7 @@ class RSFormSerializer(serializers.ModelSerializer):
|
|||
cst = schema.insert_new(cst_data['alias'])
|
||||
old_id = cst_data['id']
|
||||
cst_data['id'] = cst.pk
|
||||
cst_data['schema'] = cast(LibraryItem, self.instance).pk
|
||||
new_cst = CstBaseSerializer(data=cst_data)
|
||||
new_cst.is_valid(raise_exception=True)
|
||||
new_cst.validated_data['order'] = ids.index(old_id)
|
||||
|
|
633
rsconcept/frontend/package-lock.json
generated
633
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -14,13 +14,14 @@
|
|||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@uiw/codemirror-themes": "^4.23.6",
|
||||
"@uiw/react-codemirror": "^4.23.6",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@uiw/codemirror-themes": "^4.23.7",
|
||||
"@uiw/react-codemirror": "^4.23.7",
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "^2.1.1",
|
||||
"html-to-image": "^1.11.11",
|
||||
"js-file-download": "^0.4.12",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
|
@ -29,7 +30,7 @@
|
|||
"react-router": "^7.0.2",
|
||||
"react-select": "^5.9.0",
|
||||
"react-tabs": "^6.0.2",
|
||||
"react-toastify": "^10.0.6",
|
||||
"react-toastify": "^11.0.0",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-zoom-pan-pinch": "^3.6.1",
|
||||
"reactflow": "^11.11.4",
|
||||
|
@ -46,7 +47,7 @@
|
|||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"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-compiler": "^19.0.0-beta-37ed2a7-20241206",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
|
@ -54,10 +55,10 @@
|
|||
"globals": "^15.13.0",
|
||||
"jest": "^29.7.0",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.18.0",
|
||||
"typescript-eslint": "^8.18.1",
|
||||
"vite": "^6.0.3"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
|
@ -10,12 +10,13 @@ import { NavigationState } from '@/context/NavigationContext';
|
|||
import { globals } from '@/utils/constants';
|
||||
|
||||
function ApplicationLayout() {
|
||||
const { viewportHeight, mainHeight, showScroll } = useConceptOptions();
|
||||
const { viewportHeight, mainHeight, showScroll, noNavigationAnimation } = useConceptOptions();
|
||||
return (
|
||||
<NavigationState>
|
||||
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
||||
<ConceptToaster
|
||||
className='mt-[4rem] text-[14px]' // prettier: split lines
|
||||
className='text-[14px] cc-animate-position'
|
||||
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
||||
autoClose={3000}
|
||||
draggable={false}
|
||||
pauseOnFocusLoss={false}
|
||||
|
|
|
@ -31,9 +31,8 @@ function NavigationButton({
|
|||
className={clsx(
|
||||
'mr-1 h-full', // prettier: split lines
|
||||
'flex items-center gap-1',
|
||||
'clr-btn-nav cc-animate-color',
|
||||
'clr-btn-nav cc-animate-color duration-500',
|
||||
'rounded-xl',
|
||||
'transition duration-500',
|
||||
'font-controls whitespace-nowrap',
|
||||
{
|
||||
'px-2': text,
|
||||
|
|
|
@ -1,35 +1,53 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { IconPin, IconUnpin } from '@/components/Icons';
|
||||
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { globals, PARAMETER } from '@/utils/constants';
|
||||
|
||||
function ToggleNavigation() {
|
||||
const { noNavigationAnimation, toggleNoNavigation } = useConceptOptions();
|
||||
const { noNavigationAnimation, noNavigation, toggleNoNavigation, toggleDarkMode, darkMode } = useConceptOptions();
|
||||
const iconSize = !noNavigationAnimation ? '0.75rem' : '1rem';
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
tabIndex={-1}
|
||||
<div
|
||||
className={clsx(
|
||||
'absolute top-0 right-0 z-navigation',
|
||||
'min-h-[2rem] min-w-[2rem] sm:min-w-fit',
|
||||
'flex items-center justify-center',
|
||||
'clr-hover',
|
||||
'select-none'
|
||||
'min-h-[2rem] min-w-[2rem]',
|
||||
'flex items-end justify-center gap-1',
|
||||
'select-none',
|
||||
!noNavigation && 'flex-col-reverse'
|
||||
)}
|
||||
onClick={toggleNoNavigation}
|
||||
data-tooltip-id={globals.tooltip}
|
||||
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||
style={{
|
||||
transitionProperty: 'height, width, background-color',
|
||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||
height: noNavigationAnimation ? '1.2rem' : '3rem',
|
||||
width: noNavigationAnimation ? '3rem' : '1.2rem'
|
||||
height: noNavigationAnimation ? '2rem' : '3rem',
|
||||
width: noNavigationAnimation ? '3rem' : '2rem'
|
||||
}}
|
||||
>
|
||||
{!noNavigationAnimation ? <IconPin /> : null}
|
||||
{noNavigationAnimation ? <IconUnpin /> : null}
|
||||
{!noNavigationAnimation ? (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ export { TbEye as IconShow } from 'react-icons/tb';
|
|||
export { TbEyeX as IconHide } from 'react-icons/tb';
|
||||
export { BiShareAlt as IconShare } from 'react-icons/bi';
|
||||
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 {BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
|
||||
export { LuTriangleAlert as IconAlert } from 'react-icons/lu';
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { CstClass, IConstituenta } from '@/models/rsform';
|
||||
import { APP_COLORS, colorFgCstStatus } from '@/styling/color';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
import { CProps } from '../props';
|
||||
import TooltipConstituenta from './TooltipConstituenta';
|
||||
|
||||
interface BadgeConstituentaProps extends CProps.Styling {
|
||||
/** Prefix for tooltip ID. */
|
||||
|
@ -18,6 +19,8 @@ interface BadgeConstituentaProps extends CProps.Styling {
|
|||
* Displays a badge with a constituenta alias and information tooltip.
|
||||
*/
|
||||
function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstituentaProps) {
|
||||
const { setHoverCst } = useConceptOptions();
|
||||
|
||||
return (
|
||||
<div
|
||||
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,
|
||||
...style
|
||||
}}
|
||||
data-tooltip-id={globals.constituenta_tooltip}
|
||||
onMouseEnter={() => setHoverCst(value)}
|
||||
>
|
||||
{value.alias}
|
||||
<TooltipConstituenta anchor={`#${prefixID}${value.id}`} data={value} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -65,6 +65,7 @@ function Checkbox({
|
|||
<div
|
||||
className={clsx(
|
||||
'max-w-[1rem] min-w-[1rem] h-4', // prettier: split lines
|
||||
'pt-[0.1rem] pl-[0.1rem]',
|
||||
'border rounded-sm',
|
||||
'cc-animate-color',
|
||||
{
|
||||
|
@ -73,11 +74,7 @@ function Checkbox({
|
|||
}
|
||||
)}
|
||||
>
|
||||
{value ? (
|
||||
<div className='mt-[1px] ml-[1px]'>
|
||||
<CheckboxChecked />
|
||||
</div>
|
||||
) : null}
|
||||
{value ? <CheckboxChecked /> : null}
|
||||
</div>
|
||||
{label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null}
|
||||
</button>
|
||||
|
|
|
@ -66,6 +66,7 @@ function CheckboxTristate({
|
|||
<div
|
||||
className={clsx(
|
||||
'w-4 h-4', // prettier: split lines
|
||||
'pt-[0.1rem] pl-[0.1rem]',
|
||||
'border rounded-sm',
|
||||
'cc-animate-color',
|
||||
{
|
||||
|
@ -74,16 +75,8 @@ function CheckboxTristate({
|
|||
}
|
||||
)}
|
||||
>
|
||||
{value ? (
|
||||
<div className='mt-[1px] ml-[1px]'>
|
||||
<CheckboxChecked />
|
||||
</div>
|
||||
) : null}
|
||||
{value == null ? (
|
||||
<div className='mt-[1px] ml-[1px]'>
|
||||
<CheckboxNull />
|
||||
</div>
|
||||
) : null}
|
||||
{value ? <CheckboxChecked /> : null}
|
||||
{value == null ? <CheckboxNull /> : null}
|
||||
</div>
|
||||
{label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null}
|
||||
</button>
|
||||
|
|
|
@ -12,7 +12,6 @@ import BadgeHelp from '../info/BadgeHelp';
|
|||
import { CProps } from '../props';
|
||||
import Button from './Button';
|
||||
import MiniButton from './MiniButton';
|
||||
import Overlay from './Overlay';
|
||||
|
||||
export interface ModalProps extends CProps.Styling {
|
||||
/** Title of the modal window. */
|
||||
|
@ -105,19 +104,19 @@ function Modal({
|
|||
'border rounded-xl bg-prim-100'
|
||||
)}
|
||||
>
|
||||
<Overlay position='right-2 top-2'>
|
||||
{helpTopic && !hideHelpWhen?.() ? (
|
||||
<div className='float-left mt-2 ml-2'>
|
||||
<BadgeHelp topic={helpTopic} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} padding='p-0' />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<MiniButton
|
||||
noPadding
|
||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='float-right mt-2 mr-2'
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
</Overlay>
|
||||
{helpTopic && !hideHelpWhen?.() ? (
|
||||
<Overlay position='left-2 top-2'>
|
||||
<BadgeHelp topic={helpTopic} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} padding='p-0' />
|
||||
</Overlay>
|
||||
) : null}
|
||||
|
||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ function TabLabel({ label, title, titleHtml, hideTitle, className, ...otherProps
|
|||
className={clsx(
|
||||
'min-w-[5.5rem] h-full',
|
||||
'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',
|
||||
'select-none hover:cursor-pointer',
|
||||
'outline-none',
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
'use client';
|
||||
|
||||
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 useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
import { globals, PARAMETER, storage } from '@/utils/constants';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
|
@ -36,6 +40,8 @@ interface IOptionsContext {
|
|||
location: string;
|
||||
setLocation: React.Dispatch<React.SetStateAction<string>>;
|
||||
|
||||
setHoverCst: (newValue: IConstituenta | undefined) => void;
|
||||
|
||||
calculateHeight: (offset: string, minimum?: string) => string;
|
||||
}
|
||||
|
||||
|
@ -61,6 +67,8 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
|
|||
const [noFooter, setNoFooter] = useState(false);
|
||||
const [showScroll, setShowScroll] = useState(false);
|
||||
|
||||
const [hoverCst, setHoverCst] = useState<IConstituenta | undefined>(undefined);
|
||||
|
||||
function setDarkClass(isDark: boolean) {
|
||||
const root = window.document.documentElement;
|
||||
if (isDark) {
|
||||
|
@ -99,7 +107,26 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
|
|||
);
|
||||
|
||||
const toggleDarkMode = useCallback(() => {
|
||||
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]);
|
||||
|
||||
const mainHeight = useMemo(() => {
|
||||
|
@ -138,23 +165,27 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
|
|||
toggleShowHelp: () => setShowHelp(prev => !prev),
|
||||
viewportHeight,
|
||||
mainHeight,
|
||||
calculateHeight
|
||||
calculateHeight,
|
||||
setHoverCst
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Tooltip
|
||||
float // prettier: split-lines
|
||||
id={`${globals.tooltip}`}
|
||||
float
|
||||
id={globals.tooltip}
|
||||
layer='z-topmost'
|
||||
place='right-start'
|
||||
className='mt-8 max-w-[20rem] break-words'
|
||||
/>
|
||||
<Tooltip
|
||||
float
|
||||
id={`${globals.value_tooltip}`}
|
||||
id={globals.value_tooltip}
|
||||
layer='z-topmost'
|
||||
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}
|
||||
</>
|
||||
|
|
|
@ -140,7 +140,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
|
|||
selectedIndex={activeTab}
|
||||
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]' />
|
||||
|
|
|
@ -110,7 +110,9 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
|
|||
selectedIndex={activeTab}
|
||||
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
|
||||
title={describeOperationType(OperationType.INPUT)}
|
||||
label={labelOperationType(OperationType.INPUT)}
|
||||
|
|
|
@ -95,7 +95,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
}, [schemasIDs, needPreload, cache]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cache.loading || schemas.length !== schemasIDs.length) {
|
||||
if (cache.loading || schemas.length !== schemasIDs.length || schemas.length === 0) {
|
||||
return;
|
||||
}
|
||||
setSubstitutions(prev =>
|
||||
|
@ -114,7 +114,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
}, [schemasIDs, schemas, cache.loading, getSchemaByCst]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cache.loading || schemas.length !== schemasIDs.length) {
|
||||
if (cache.loading || schemas.length !== schemasIDs.length || schemas.length === 0) {
|
||||
return;
|
||||
}
|
||||
const validator = new SubstitutionValidator(schemas, substitutions);
|
||||
|
@ -155,7 +155,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
selectedIndex={activeTab}
|
||||
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]' />
|
||||
{target.operation_type === OperationType.SYNTHESIS ? (
|
||||
<TabLabel title='Выбор аргументов операции' label='Аргументы' className='w-[8rem]' />
|
||||
|
|
|
@ -44,7 +44,7 @@ function TabSynthesis({
|
|||
disabled
|
||||
value={validationText}
|
||||
rows={4}
|
||||
style={{ borderColor: isCorrect ? undefined : APP_COLORS.fgRed }}
|
||||
style={{ borderColor: isCorrect ? undefined : APP_COLORS.fgRed, borderWidth: isCorrect ? undefined : '2px' }}
|
||||
/>
|
||||
</div>
|
||||
</DataLoader>
|
||||
|
|
|
@ -55,7 +55,7 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
|
|||
selectedIndex={activeTab}
|
||||
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='Установление синтаксической связи с отсылкой на термин'
|
||||
|
|
|
@ -70,7 +70,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
|
|||
selectedIndex={activeTab}
|
||||
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]' />
|
||||
|
|
26
rsconcept/frontend/src/dialogs/DlgShowQR.tsx
Normal file
26
rsconcept/frontend/src/dialogs/DlgShowQR.tsx
Normal 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;
|
|
@ -75,6 +75,7 @@ export class RSFormLoader {
|
|||
const order = this.graph.topologicalOrder();
|
||||
order.forEach(cstID => {
|
||||
const cst = this.cstByID.get(cstID)!;
|
||||
cst.schema = this.schema.id;
|
||||
cst.status = inferStatus(cst.parse.status, cst.parse.valueClass);
|
||||
cst.is_template = inferTemplate(cst.definition_formal);
|
||||
cst.cst_class = inferClass(cst.cst_type, cst.is_template);
|
||||
|
|
|
@ -70,9 +70,10 @@ export class SubstitutionValidator {
|
|||
constructor(schemas: IRSForm[], substitutions: ICstSubstitute[]) {
|
||||
this.schemas = schemas;
|
||||
this.substitutions = substitutions;
|
||||
if (this.substitutions.length === 0) {
|
||||
if (schemas.length === 0 || substitutions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
schemas.forEach(schema => {
|
||||
this.schemaByID.set(schema.id, schema);
|
||||
this.mapping.set(schema.id, {});
|
||||
|
|
|
@ -65,7 +65,6 @@ export interface TermForm {
|
|||
*/
|
||||
export interface IConstituentaMeta {
|
||||
id: ConstituentaID;
|
||||
schema: LibraryItemID;
|
||||
alias: string;
|
||||
convention: string;
|
||||
cst_type: CstType;
|
||||
|
@ -101,6 +100,9 @@ export interface IConstituentaData extends IConstituentaMeta {
|
|||
* Represents Constituenta.
|
||||
*/
|
||||
export interface IConstituenta extends IConstituentaData {
|
||||
/** {@link LibraryItemID} of this {@link IConstituenta}. */
|
||||
schema: LibraryItemID;
|
||||
|
||||
/** {@link CstClass} of this {@link IConstituenta}. */
|
||||
cst_class: CstClass;
|
||||
/** {@link ExpressionStatus} of this {@link IConstituenta}. */
|
||||
|
@ -135,7 +137,7 @@ export interface IConstituenta extends IConstituentaData {
|
|||
/**
|
||||
* Represents {@link IConstituenta} reference.
|
||||
*/
|
||||
export interface IConstituentaReference extends Pick<IConstituentaMeta, 'id' | 'schema'> {}
|
||||
export interface IConstituentaReference extends Pick<IConstituenta, 'id' | 'schema'> {}
|
||||
|
||||
/**
|
||||
* Represents Constituenta list.
|
||||
|
|
|
@ -117,7 +117,7 @@ function OssTabs() {
|
|||
className='flex flex-col mx-auto min-w-fit'
|
||||
>
|
||||
<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} />
|
||||
|
||||
<TabLabel label='Карточка' title={schema.title ?? ''} />
|
||||
|
|
|
@ -15,7 +15,7 @@ import FormConstituenta from './FormConstituenta';
|
|||
import ToolbarConstituenta from './ToolbarConstituenta';
|
||||
|
||||
// Threshold window width to switch layout.
|
||||
const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
|
||||
const SIDELIST_LAYOUT_THRESHOLD = 1050; // px
|
||||
|
||||
interface EditorConstituentaProps {
|
||||
activeCst?: IConstituenta;
|
||||
|
@ -87,7 +87,7 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
|||
tabIndex={-1}
|
||||
className={clsx(
|
||||
'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]',
|
||||
'overflow-y-auto overflow-x-clip',
|
||||
{ 'flex-col md:items-center': isNarrow }
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { IconDropArrow, IconDropArrowUp } from '@/components/Icons';
|
||||
import TooltipConstituenta from '@/components/info/TooltipConstituenta';
|
||||
import { CProps } from '@/components/props';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
|
@ -13,7 +12,7 @@ import useWindowSize from '@/hooks/useWindowSize';
|
|||
import { GraphColoring } from '@/models/miscellaneous';
|
||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
|
||||
import { PARAMETER, prefixes, storage } from '@/utils/constants';
|
||||
import { globals, PARAMETER, prefixes, storage } from '@/utils/constants';
|
||||
|
||||
interface ViewHiddenProps {
|
||||
items: ConstituentaID[];
|
||||
|
@ -31,6 +30,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
|||
const windowSize = useWindowSize();
|
||||
const localSelected = items.filter(id => selected.includes(id));
|
||||
const [isFolded, setIsFolded] = useLocalStorage(storage.rsgraphFoldHidden, false);
|
||||
const { setHoverCst } = useConceptOptions();
|
||||
|
||||
function handleClick(cstID: ConstituentaID, event: CProps.EventMouse) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
|
@ -89,11 +89,9 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
|||
const adjustedColoring = coloringScheme === 'none' ? 'status' : coloringScheme;
|
||||
const id = `${prefixes.cst_hidden_list}${cst.alias}`;
|
||||
return (
|
||||
<div key={`wrap-${id}`}>
|
||||
<button
|
||||
type='button'
|
||||
key={id}
|
||||
id={id}
|
||||
type='button'
|
||||
className='min-w-[3rem] rounded-md text-center select-none'
|
||||
style={{
|
||||
backgroundColor: colorBgGraphNode(cst, adjustedColoring),
|
||||
|
@ -107,11 +105,11 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
|||
}}
|
||||
onClick={event => handleClick(cstID, event)}
|
||||
onDoubleClick={() => onEdit(cstID)}
|
||||
data-tooltip-id={globals.constituenta_tooltip}
|
||||
onMouseEnter={() => setHoverCst(cst)}
|
||||
>
|
||||
{cst.alias}
|
||||
</button>
|
||||
<TooltipConstituenta data={cst} anchor={`#${id}`} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
IconNewVersion,
|
||||
IconOSS,
|
||||
IconOwner,
|
||||
IconQR,
|
||||
IconReader,
|
||||
IconReplace,
|
||||
IconShare,
|
||||
|
@ -85,6 +86,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
controller.share();
|
||||
}
|
||||
|
||||
function handleShowQR() {
|
||||
schemaMenu.hide();
|
||||
controller.showQR();
|
||||
}
|
||||
|
||||
function handleCreateVersion() {
|
||||
schemaMenu.hide();
|
||||
controller.createVersion();
|
||||
|
@ -155,10 +161,16 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
onClick={handleShare}
|
||||
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='QR-код'
|
||||
title='Показать QR-код схемы'
|
||||
icon={<IconQR size='1rem' className='icon-primary' />}
|
||||
onClick={handleShowQR}
|
||||
/>
|
||||
{user ? (
|
||||
<DropdownButton
|
||||
text='Клонировать'
|
||||
icon={<IconClone size='1rem' className='icon-primary' />}
|
||||
icon={<IconClone size='1rem' className='icon-green' />}
|
||||
disabled={model.isArchive}
|
||||
onClick={handleClone}
|
||||
/>
|
||||
|
|
|
@ -21,6 +21,7 @@ import DlgEditVersions from '@/dialogs/DlgEditVersions';
|
|||
import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
|
||||
import DlgInlineSynthesis from '@/dialogs/DlgInlineSynthesis';
|
||||
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
||||
import DlgShowQR from '@/dialogs/DlgShowQR';
|
||||
import DlgShowTypeGraph from '@/dialogs/DlgShowTypeGraph';
|
||||
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
||||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||
|
@ -108,6 +109,7 @@ export interface IRSEditContext extends ILibraryItemEditor {
|
|||
substitute: () => void;
|
||||
|
||||
showTypeGraph: () => void;
|
||||
showQR: () => void;
|
||||
}
|
||||
|
||||
const RSEditContext = createContext<IRSEditContext | null>(null);
|
||||
|
@ -172,6 +174,7 @@ export const RSEditState = ({
|
|||
const [showEditVersions, setShowEditVersions] = useState(false);
|
||||
const [showInlineSynthesis, setShowInlineSynthesis] = useState(false);
|
||||
const [showTypeGraph, setShowTypeGraph] = useState(false);
|
||||
const [showQR, setShowQR] = useState(false);
|
||||
|
||||
const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
|
||||
const [showCreateCst, setShowCreateCst] = useState(false);
|
||||
|
@ -627,6 +630,11 @@ export const RSEditState = ({
|
|||
[model]
|
||||
);
|
||||
|
||||
function generateQR(): string {
|
||||
const currentRef = window.location.href;
|
||||
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
|
||||
}
|
||||
|
||||
return (
|
||||
<RSEditContext
|
||||
value={{
|
||||
|
@ -679,11 +687,13 @@ export const RSEditState = ({
|
|||
produceStructure,
|
||||
substitute,
|
||||
|
||||
showTypeGraph: () => setShowTypeGraph(true)
|
||||
showTypeGraph: () => setShowTypeGraph(true),
|
||||
showQR: () => setShowQR(true)
|
||||
}}
|
||||
>
|
||||
{model.schema ? (
|
||||
<>
|
||||
{showQR ? <DlgShowQR hideWindow={() => setShowQR(false)} target={generateQR()} /> : null}
|
||||
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
|
||||
{showClone ? (
|
||||
<DlgCloneLibraryItem
|
||||
|
|
|
@ -197,7 +197,9 @@ function RSTabs() {
|
|||
className='flex flex-col mx-auto min-w-fit'
|
||||
>
|
||||
<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} />
|
||||
|
||||
<TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} />
|
||||
|
|
|
@ -63,7 +63,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
|
|||
<SearchBar
|
||||
id='constituents_search'
|
||||
noBorder
|
||||
className='min-w-[6rem] pr-2 flex-grow'
|
||||
className='min-w-[6rem] w-[6rem] mr-2 flex-grow'
|
||||
query={filterText}
|
||||
onChangeQuery={setFilterText}
|
||||
/>
|
||||
|
|
|
@ -2,5 +2,4 @@
|
|||
* Module: List external styling imports.
|
||||
*/
|
||||
|
||||
@import 'react-toastify/dist/ReactToastify.css';
|
||||
@import 'reactflow/dist/style.css';
|
||||
|
|
|
@ -144,6 +144,7 @@ export const storage = {
|
|||
export const globals = {
|
||||
tooltip: 'global_tooltip',
|
||||
value_tooltip: 'value_tooltip',
|
||||
constituenta_tooltip: 'cst_tooltip',
|
||||
password_tooltip: 'password_tooltip',
|
||||
email_tooltip: 'email_tooltip',
|
||||
main_scroll: 'main_scroll',
|
||||
|
|
Loading…
Reference in New Issue
Block a user