Rework UI toolbars and icons

This commit is contained in:
IRBorisov 2023-12-16 19:20:26 +03:00
parent 7282063738
commit ada335ee21
51 changed files with 503 additions and 687 deletions

View File

@ -13,6 +13,7 @@ This readme file is used mostly to document project dependencies
<pre> <pre>
- axios - axios
- clsx - clsx
- react-icons
- react-router-dom - react-router-dom
- react-toastify - react-toastify
- react-loader-spinner - react-loader-spinner

View File

@ -18,6 +18,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11", "react-error-boundary": "^4.0.11",
"react-icons": "^4.12.0",
"react-intl": "^6.5.5", "react-intl": "^6.5.5",
"react-loader-spinner": "^5.4.5", "react-loader-spinner": "^5.4.5",
"react-pdf": "^7.6.0", "react-pdf": "^7.6.0",
@ -8389,6 +8390,14 @@
"react": ">=16.13.1" "react": ">=16.13.1"
} }
}, },
"node_modules/react-icons": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz",
"integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-intl": { "node_modules/react-intl": {
"version": "6.5.5", "version": "6.5.5",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.5.5.tgz", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.5.5.tgz",

View File

@ -22,6 +22,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11", "react-error-boundary": "^4.0.11",
"react-icons": "^4.12.0",
"react-intl": "^6.5.5", "react-intl": "^6.5.5",
"react-loader-spinner": "^5.4.5", "react-loader-spinner": "^5.4.5",
"react-pdf": "^7.6.0", "react-pdf": "^7.6.0",

View File

@ -1,4 +1,5 @@
import { MagnifyingGlassIcon } from '../Icons'; import { BiSearchAlt2 } from 'react-icons/bi';
import Overlay from './Overlay'; import Overlay from './Overlay';
import TextInput from './TextInput'; import TextInput from './TextInput';
@ -16,7 +17,7 @@ function ConceptSearch({ value, onChange, noBorder, dimensions }: ConceptSearchP
position='top-[-0.125rem] left-3 translate-y-1/2' position='top-[-0.125rem] left-3 translate-y-1/2'
className='pointer-events-none clr-text-controls' className='pointer-events-none clr-text-controls'
> >
<MagnifyingGlassIcon size={5} /> <BiSearchAlt2 size='1.25rem' />
</Overlay> </Overlay>
<TextInput noOutline <TextInput noOutline
placeholder='Поиск' placeholder='Поиск'

View File

@ -25,14 +25,15 @@ function ConceptTooltip({
} }
return createPortal( return createPortal(
<Tooltip <Tooltip
delayShow={500}
opacity={0.97} opacity={0.97}
style={{...{ paddingTop: '2px', paddingBottom: '2px'}, ...style}}
className={clsx( className={clsx(
'overflow-auto', 'overflow-auto',
'border shadow-md', 'border shadow-md',
layer, layer,
className className
)} )}
style={{...{ paddingTop: '2px', paddingBottom: '2px'}, ...style}}
variant={(darkMode ? 'dark' : 'light')} variant={(darkMode ? 'dark' : 'light')}
place={place} place={place}
{...restProps} {...restProps}

View File

@ -1,30 +1,43 @@
import clsx from 'clsx'; import clsx from 'clsx';
interface DropdownButtonProps { interface DropdownButtonProps {
text?: string
icon?: React.ReactNode
className?: string
tooltip?: string | undefined tooltip?: string | undefined
onClick?: () => void onClick?: () => void
disabled?: boolean disabled?: boolean
children: React.ReactNode
children?: React.ReactNode
} }
function DropdownButton({ tooltip, onClick, disabled, children }: DropdownButtonProps) { function DropdownButton({
text, icon, children,
tooltip, className,
disabled,
onClick
}: DropdownButtonProps) {
return ( return (
<button type='button' <button type='button'
disabled={disabled} disabled={disabled}
title={tooltip} title={tooltip}
onClick={onClick} onClick={onClick}
className={clsx( className={clsx(
'px-3 py-1', 'px-3 py-1 inline-flex items-center gap-2',
'text-left overflow-ellipsis whitespace-nowrap', 'text-left text-sm overflow-ellipsis whitespace-nowrap',
'disabled:clr-text-controls', 'disabled:clr-text-controls',
{ {
'clr-hover': onClick, 'clr-hover': onClick,
'cursor-pointer disabled:cursor-not-allowed': onClick, 'cursor-pointer disabled:cursor-not-allowed': onClick,
'cursor-default': !onClick 'cursor-default': !onClick
} },
className
)} )}
> >
{children} {children ? children : null}
{!children && icon ? icon : null}
{!children && text ? <span>{text}</span> : null}
</button>); </button>);
} }

View File

@ -2,8 +2,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { BiUpload } from 'react-icons/bi';
import { UploadIcon } from '../Icons';
import Button from './Button'; import Button from './Button';
import Label from './Label'; import Label from './Label';
@ -55,7 +55,7 @@ function FileInput({
/> />
<Button <Button
text={label} text={label}
icon={<UploadIcon/>} icon={<BiUpload size='1.5rem' />}
onClick={handleUploadClick} onClick={handleUploadClick}
tooltip={tooltip} tooltip={tooltip}
/> />

View File

@ -2,10 +2,10 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useRef } from 'react'; import { useRef } from 'react';
import { BiX } from 'react-icons/bi';
import useEscapeKey from '@/hooks/useEscapeKey'; import useEscapeKey from '@/hooks/useEscapeKey';
import { CrossIcon } from '../Icons';
import Button from './Button'; import Button from './Button';
import MiniButton from './MiniButton'; import MiniButton from './MiniButton';
import Overlay from './Overlay'; import Overlay from './Overlay';
@ -62,7 +62,7 @@ function Modal({
<Overlay position='right-[0.3rem] top-2' className='text-disabled'> <Overlay position='right-[0.3rem] top-2' className='text-disabled'>
<MiniButton <MiniButton
tooltip='Закрыть диалоговое окно [ESC]' tooltip='Закрыть диалоговое окно [ESC]'
icon={<CrossIcon size={5}/>} icon={<BiX size='1.25rem'/>}
onClick={handleCancel} onClick={handleCancel}
/> />
</Overlay> </Overlay>

View File

@ -1,4 +1,4 @@
import { GotoFirstIcon, GotoLastIcon, GotoNextIcon, GotoPrevIcon } from '@/components/Icons'; import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-icons/bi';
interface PageControlsProps { interface PageControlsProps {
pageNumber: number pageNumber: number
@ -16,14 +16,14 @@ function PageControls({
onClick={() => setPageNumber(1)} onClick={() => setPageNumber(1)}
disabled={pageNumber < 2} disabled={pageNumber < 2}
> >
<GotoFirstIcon /> <BiFirstPage size='1.5rem' />
</button> </button>
<button type='button' <button type='button'
className='clr-hover clr-text-controls' className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev - 1)} onClick={() => setPageNumber(prev => prev - 1)}
disabled={pageNumber < 2} disabled={pageNumber < 2}
> >
<GotoPrevIcon /> <BiChevronLeft size='1.5rem' />
</button> </button>
<p className='px-3 text-black'>Страница {pageNumber} из {pageCount}</p> <p className='px-3 text-black'>Страница {pageNumber} из {pageCount}</p>
<button type='button' <button type='button'
@ -31,14 +31,14 @@ function PageControls({
onClick={() => setPageNumber(prev => prev + 1)} onClick={() => setPageNumber(prev => prev + 1)}
disabled={pageNumber >= pageCount} disabled={pageNumber >= pageCount}
> >
<GotoNextIcon /> <BiChevronRight size='1.5rem' />
</button> </button>
<button type='button' <button type='button'
className='clr-hover clr-text-controls' className='clr-hover clr-text-controls'
onClick={() => setPageNumber(pageCount)} onClick={() => setPageNumber(pageCount)}
disabled={pageNumber >= pageCount} disabled={pageNumber >= pageCount}
> >
<GotoLastIcon /> <BiLastPage size='1.5rem' />
</button> </button>
</>); </>);
} }

View File

@ -3,11 +3,10 @@
import { Table } from '@tanstack/react-table'; import { Table } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-icons/bi';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { GotoFirstIcon, GotoLastIcon, GotoNextIcon, GotoPrevIcon } from '../Icons';
interface PaginationToolsProps<TData> { interface PaginationToolsProps<TData> {
table: Table<TData> table: Table<TData>
paginationOptions: number[] paginationOptions: number[]
@ -47,14 +46,14 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
onClick={() => table.setPageIndex(0)} onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
<GotoFirstIcon /> <BiFirstPage size='1.5rem' />
</button> </button>
<button type='button' <button type='button'
className='clr-hover clr-text-controls' className='clr-hover clr-text-controls'
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
<GotoPrevIcon /> <BiChevronLeft size='1.5rem' />
</button> </button>
<input <input
title='Номер страницы. Выделите для ручного ввода' title='Номер страницы. Выделите для ручного ввода'
@ -72,14 +71,14 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
<GotoNextIcon /> <BiChevronRight size='1.5rem' />
</button> </button>
<button type='button' <button type='button'
className='clr-hover clr-text-controls' className='clr-hover clr-text-controls'
onClick={() => table.setPageIndex(table.getPageCount() - 1)} onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
<GotoLastIcon /> <BiLastPage size='1.5rem' />
</button> </button>
</div> </div>
<select <select

View File

@ -9,10 +9,10 @@ interface SortingIconProps<TData> {
function SortingIcon<TData>({ column }: SortingIconProps<TData>) { function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
return (<> return (<>
{{ {{
desc: <DescendingIcon size={4} />, desc: <DescendingIcon size='1rem' />,
asc: <AscendingIcon size={4}/>, asc: <AscendingIcon size='1rem'/>,
}[column.getIsSorted() as string] ?? }[column.getIsSorted() as string] ??
<DescendingIcon size={4} color='opacity-0 hover:opacity-50' /> <DescendingIcon size='1rem' className='opacity-0 hover:opacity-50' />
} }
</>); </>);
} }

View File

@ -9,7 +9,7 @@ interface ConstituentaTooltipProps {
function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) { function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
return ( return (
<ConceptTooltip <ConceptTooltip clickable
anchorSelect={anchor} anchorSelect={anchor}
className='max-w-[25rem] min-w-[25rem]' className='max-w-[25rem] min-w-[25rem]'
> >

View File

@ -1,6 +1,7 @@
import { BiInfoCircle } from 'react-icons/bi';
import ConceptTooltip from '@/components/Common/ConceptTooltip'; import ConceptTooltip from '@/components/Common/ConceptTooltip';
import TextURL from '@/components/Common/TextURL'; import TextURL from '@/components/Common/TextURL';
import { HelpIcon } from '@/components/Icons';
import { HelpTopic } from '@/models/miscelanious'; import { HelpTopic } from '@/models/miscelanious';
import InfoTopic from './InfoTopic'; import InfoTopic from './InfoTopic';
@ -18,7 +19,7 @@ function HelpButton({ topic, offset, dimensions }: HelpButtonProps) {
id={`help-${topic}`} id={`help-${topic}`}
className='p-1' className='p-1'
> >
<HelpIcon color='clr-text-primary' size={5} /> <BiInfoCircle size='1.25rem' className='clr-text-primary' />
</div> </div>
<ConceptTooltip clickable <ConceptTooltip clickable
anchorSelect={`#help-${topic}`} anchorSelect={`#help-${topic}`}

View File

@ -9,15 +9,15 @@ function HelpLibrary() {
<p>На текущем этапе происходит наполнение Библиотеки концептуальными схемами.</p> <p>На текущем этапе происходит наполнение Библиотеки концептуальными схемами.</p>
<p>Поиск осуществлеяется с помощью инструментов в верхней части страницы.</p> <p>Поиск осуществлеяется с помощью инструментов в верхней части страницы.</p>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<SubscribedIcon size={4}/> <SubscribedIcon size='1rem'/>
<p>Аттрибут <b>отслеживаемая</b> обозначает отслеживание схемы.</p> <p>Аттрибут <b>отслеживаемая</b> обозначает отслеживание схемы.</p>
</div> </div>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<GroupIcon size={4}/> <GroupIcon size='1rem'/>
<p>Аттрибут <b>общедоступная</b> делает схему видимой в разделе библиотека.</p> <p>Аттрибут <b>общедоступная</b> делает схему видимой в разделе библиотека.</p>
</div> </div>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<EducationIcon size={4}/> <EducationIcon size='1rem'/>
<p>Аттрибут <b>неизменная</b> выделяет стандартные схемы.</p> <p>Аттрибут <b>неизменная</b> выделяет стандартные схемы.</p>
</div> </div>
</div>); </div>);

View File

@ -2,24 +2,23 @@
interface IconSVGProps { interface IconSVGProps {
viewbox: string viewbox: string
size?: number size?: string
color?: string className?: string
props?: React.SVGProps<SVGSVGElement> props?: React.SVGProps<SVGSVGElement>
children: React.ReactNode children: React.ReactNode
} }
export interface IconProps { export interface IconProps {
size?: number size?: string
color?: string className?: string
} }
function IconSVG({ viewbox, size = 6, color, props, children }: IconSVGProps) { function IconSVG({ viewbox, size = '1.5rem', className, props, children }: IconSVGProps) {
const width = `${size * 1 / 4}rem`;
return ( return (
<svg <svg
width={width} width={size}
height={width} height={size}
className={`w-[${width}] h-[${width}] ${color}`} className={`w-[${size}] h-[${size}] ${className}`}
fill='currentColor' fill='currentColor'
viewBox={viewbox} viewBox={viewbox}
{...props} {...props}
@ -28,39 +27,6 @@ function IconSVG({ viewbox, size = 6, color, props, children }: IconSVGProps) {
</svg>); </svg>);
} }
export function MagnifyingGlassIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' {...props} >
<path d='M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z'/>
</IconSVG>
);
}
export function BellIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z' />
</IconSVG>
);
}
export function EyeIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M14 12c-1.095 0-2-.905-2-2 0-.354.103-.683.268-.973C12.178 9.02 12.092 9 12 9a3.02 3.02 0 00-3 3c0 1.642 1.358 3 3 3 1.641 0 3-1.358 3-3 0-.092-.02-.178-.027-.268-.29.165-.619.268-.973.268z' />
<path d='M12 5c-7.633 0-9.927 6.617-9.948 6.684L1.946 12l.105.316C2.073 12.383 4.367 19 12 19s9.927-6.617 9.948-6.684l.106-.316-.105-.316C21.927 11.617 19.633 5 12 5zm0 12c-5.351 0-7.424-3.846-7.926-5C4.578 10.842 6.652 7 12 7c5.351 0 7.424 3.846 7.926 5-.504 1.158-2.578 5-7.926 5z' />
</IconSVG>
);
}
export function EyeOffIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 19c.946 0 1.81-.103 2.598-.281l-1.757-1.757c-.273.021-.55.038-.841.038-5.351 0-7.424-3.846-7.926-5a8.642 8.642 0 011.508-2.297L4.184 8.305c-1.538 1.667-2.121 3.346-2.132 3.379a.994.994 0 000 .633C2.073 12.383 4.367 19 12 19zm0-14c-1.837 0-3.346.396-4.604.981L3.707 2.293 2.293 3.707l18 18 1.414-1.414-3.319-3.319c2.614-1.951 3.547-4.615 3.561-4.657a.994.994 0 000-.633C21.927 11.617 19.633 5 12 5zm4.972 10.558l-2.28-2.28c.19-.39.308-.819.308-1.278 0-1.641-1.359-3-3-3-.459 0-.888.118-1.277.309L8.915 7.501A9.26 9.26 0 0112 7c5.351 0 7.424 3.846 7.926 5-.302.692-1.166 2.342-2.954 3.558z'/>
</IconSVG>
);
}
export function SubscribedIcon(props: IconProps) { export function SubscribedIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 24 24' {...props}> <IconSVG viewbox='0 0 24 24' {...props}>
@ -85,22 +51,6 @@ export function ASTNetworkIcon(props: IconProps) {
); );
} }
export function EditIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M6.3 12.3l10-10a1 1 0 011.4 0l4 4a1 1 0 010 1.4l-10 10a1 1 0 01-.7.3H7a1 1 0 01-1-1v-4a1 1 0 01.3-.7zM8 16h2.59l9-9L17 4.41l-9 9V16zm10-2a1 1 0 012 0v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6c0-1.1.9-2 2-2h6a1 1 0 010 2H4v14h14v-6z' />
</IconSVG>
);
}
export function SquaresIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z' />
</IconSVG>
);
}
export function GroupIcon(props: IconProps) { export function GroupIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 20 20' {...props}> <IconSVG viewbox='0 0 20 20' {...props}>
@ -109,30 +59,6 @@ export function GroupIcon(props: IconProps) {
); );
} }
export function FrameIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M5 3a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2V5a2 2 0 00-2-2H5zm0 2h10v7h-2l-1 2H8l-1-2H5V5z' />
</IconSVG>
);
}
export function AsteriskIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z' />
</IconSVG>
);
}
export function MenuIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z' />
</IconSVG>
);
}
export function ShareIcon(props: IconProps) { export function ShareIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 24 24' {...props}> <IconSVG viewbox='0 0 24 24' {...props}>
@ -141,14 +67,6 @@ export function ShareIcon(props: IconProps) {
); );
} }
export function FilterIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M21 3H5a1 1 0 00-1 1v2.59c0 .523.213 1.037.583 1.407L10 13.414V21a1.001 1.001 0 001.447.895l4-2c.339-.17.553-.516.553-.895v-5.586l5.417-5.417c.37-.37.583-.884.583-1.407V4a1 1 0 00-1-1zm-6.707 9.293A.996.996 0 0014 13v5.382l-2 1V13a.996.996 0 00-.293-.707L6 6.59V5h14.001l.002 1.583-5.71 5.71z' />
</IconSVG>
);
}
export function SortIcon(props: IconProps) { export function SortIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 24 24' {...props}> <IconSVG viewbox='0 0 24 24' {...props}>
@ -157,14 +75,6 @@ export function SortIcon(props: IconProps) {
); );
} }
export function BookmarkIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M5 2a2 2 0 00-2 2v14l3.5-2 3.5 2 3.5-2 3.5 2V4a2 2 0 00-2-2H5zm2.5 3a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm6.207.293a1 1 0 00-1.414 0l-6 6a1 1 0 101.414 1.414l6-6a1 1 0 000-1.414zM12.5 10a1.5 1.5 0 100 3 1.5 1.5 0 000-3z' />
</IconSVG>
);
}
export function UserIcon(props: IconProps) { export function UserIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 512 512' {...props}> <IconSVG viewbox='0 0 512 512' {...props}>
@ -181,22 +91,6 @@ export function EducationIcon(props: IconProps) {
); );
} }
export function DarkThemeIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z' />
</IconSVG>
);
}
export function LightThemeIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z' />
</IconSVG>
);
}
export function LibraryIcon(props: IconProps) { export function LibraryIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 512 512' {...props}> <IconSVG viewbox='0 0 512 512' {...props}>
@ -215,66 +109,6 @@ export function PlusIcon(props: IconProps) {
); );
} }
export function SmallPlusIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12zm10-8a8 8 0 100 16 8 8 0 000-16z'/>
<path d='M13 7a1 1 0 10-2 0v4H7a1 1 0 100 2h4v4a1 1 0 102 0v-4h4a1 1 0 100-2h-4V7z'/>
</IconSVG>
);
}
export function ArrowDropdownIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 1.993C6.486 1.994 2 6.48 2 11.994c0 5.513 4.486 9.999 10 10 5.514 0 10-4.486 10-10s-4.485-10-10-10.001zm0 18.001c-4.411-.001-8-3.59-8-8 0-4.411 3.589-8 8-8.001 4.411.001 8 3.59 8 8.001s-3.589 8-8 8z' />
<path d='M13 8h-2v4H7.991l4.005 4.005L16 12h-3z' />
</IconSVG>
);
}
export function UploadIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M11 15h2V9h3l-4-5-4 5h3z'/>
<path d='M20 18H4v-7H2v7c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2v-7h-2v7z'/>
</IconSVG>
);
}
export function DownloadIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 16l4-5h-3V4h-2v7H8z'/>
<path d='M20 18H4v-7H2v7c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2v-7h-2v7z'/>
</IconSVG>
);
}
export function OwnerIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M20.787 9.023c-.125.027-1.803.418-3.953 1.774-.323-1.567-1.279-4.501-4.108-7.485L12 2.546l-.726.767C8.435 6.308 7.483 9.25 7.163 10.827 5.005 9.448 3.34 9.052 3.218 9.024L2 8.752V10c0 7.29 3.925 12 10 12 5.981 0 10-4.822 10-12V8.758l-1.213.265zM8.999 12.038c.002-.033.152-3.1 3.001-6.532C14.814 8.906 14.999 12 15 12v.125a18.933 18.933 0 00-3.01 3.154 19.877 19.877 0 00-2.991-3.113v-.128zM12 20c-5.316 0-7.549-4.196-7.937-8.564 1.655.718 4.616 2.426 7.107 6.123l.841 1.249.825-1.26c2.426-3.708 5.425-5.411 7.096-6.122C19.534 15.654 17.304 20 12 20z' />
</IconSVG>
);
}
export function ArrowUpIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12.781 2.375c-.381-.475-1.181-.475-1.562 0l-8 10A1.001 1.001 0 004 14h4v7a1 1 0 001 1h6a1 1 0 001-1v-7h4a1.001 1.001 0 00.781-1.625l-8-10zM15 12h-1v8h-4v-8H6.081L12 4.601 17.919 12H15z' />
</IconSVG>
);
}
export function ArrowDownIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M20.901 10.566A1.001 1.001 0 0020 10h-4V3a1 1 0 00-1-1H9a1 1 0 00-1 1v7H4a1.001 1.001 0 00-.781 1.625l8 10a1 1 0 001.562 0l8-10c.24-.301.286-.712.12-1.059zM12 19.399L6.081 12H10V4h4v8h3.919L12 19.399z' />
</IconSVG>
);
}
export function ArrowLeftIcon(props: IconProps) { export function ArrowLeftIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 24 24' {...props}> <IconSVG viewbox='0 0 24 24' {...props}>
@ -291,49 +125,6 @@ export function ArrowRightIcon(props: IconProps) {
); );
} }
export function CloneIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M11 10H9v3H6v2h3v3h2v-3h3v-2h-3z' />
<path d='M4 22h12c1.103 0 2-.897 2-2V8c0-1.103-.897-2-2-2H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2zM4 8h12l.002 12H4V8z' />
<path d='M20 2H8v2h12v12h2V4c0-1.103-.897-2-2-2z' />
</IconSVG>
);
}
export function DiamondIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M17.813 3.838A2 2 0 0016.187 3H7.813c-.644 0-1.252.313-1.667.899l-4 6.581a.999.999 0 00.111 1.188l9 10a.995.995 0 001.486.001l9-10a.997.997 0 00.111-1.188l-4.041-6.643zM12 19.505L5.245 12h13.509L12 19.505zM4.777 10l3.036-5 8.332-.062L19.222 10H4.777z' />
</IconSVG>
);
}
export function DumpBinIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M5 20a2 2 0 002 2h10a2 2 0 002-2V8h2V6h-4V4a2 2 0 00-2-2H9a2 2 0 00-2 2v2H3v2h2zM9 4h6v2H9zM8 8h9v12H7V8z' />
<path d='M9 10h2v8H9zm4 0h2v8h-2z' />
</IconSVG>
);
}
export function ArrowsRotateIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 6v3l4-4-4-4v3a8 8 0 00-8 8c0 1.57.46 3.03 1.24 4.26L6.7 14.8A5.9 5.9 0 016 12a6 6 0 016-6m6.76 1.74L17.3 9.2c.44.84.7 1.8.7 2.8a6 6 0 01-6 6v-3l-4 4 4 4v-3a8 8 0 008-8c0-1.57-.46-3.03-1.24-4.26z' />
</IconSVG>
);
}
export function ArrowsFocusIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M16.121 6.465L14 4.344V10h5.656l-2.121-2.121 3.172-3.172-1.414-1.414zM4.707 3.293L3.293 4.707l3.172 3.172L4.344 10H10V4.344L7.879 6.465zM19.656 14H14v5.656l2.121-2.121 3.172 3.172 1.414-1.414-3.172-3.172zM6.465 16.121l-3.172 3.172 1.414 1.414 3.172-3.172L10 19.656V14H4.344z' />
</IconSVG>
);
}
export function LetterAIcon(props: IconProps) { export function LetterAIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 24 24' {...props}> <IconSVG viewbox='0 0 24 24' {...props}>
@ -350,48 +141,6 @@ export function LetterALinesIcon(props: IconProps) {
); );
} }
export function PlanetIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M2.76 20.2a2.73 2.73 0 002.15.85 8.86 8.86 0 003.37-.86 9 9 0 0012.27-10.9c1.31-2.23 1.75-4.26.67-5.48a2.94 2.94 0 00-2.57-1A5 5 0 0016.1 4 9 9 0 003.58 15.14c-1.06 1.21-2.05 3.68-.82 5.06zm1.5-1.32c-.22-.25 0-1.07.37-1.76a9.26 9.26 0 001.57 1.74c-1.03.3-1.71.28-1.94.02zm14.51-5.17A7 7 0 0115.58 18 7.12 7.12 0 0112 19a6.44 6.44 0 01-1.24-.13 30.73 30.73 0 004.42-3.29 31.5 31.5 0 003.8-4 6.88 6.88 0 01-.21 2.13zm.09-8.89a.94.94 0 01.87.32c.23.26.16.94-.26 1.93a9.2 9.2 0 00-1.61-1.86 2.48 2.48 0 011-.39zM5.22 10.31A6.94 6.94 0 018.41 6 7 7 0 0112 5a6.9 6.9 0 016 3.41 5.19 5.19 0 01.35.66 27.43 27.43 0 01-4.49 5A27.35 27.35 0 018.35 18a7 7 0 01-3.13-7.65z' />
</IconSVG>
);
}
export function SaveIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M5 21h14a2 2 0 002-2V8a1 1 0 00-.29-.71l-4-4A1 1 0 0016 3H5a2 2 0 00-2 2v14a2 2 0 002 2zm10-2H9v-5h6zM13 7h-2V5h2zM5 5h2v4h8V5h.59L19 8.41V19h-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5H5z' />
</IconSVG>
);
}
export function HelpIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8z' />
<path d='M11 11h2v6h-2zm0-4h2v2h-2z' />
</IconSVG>
);
}
export function GithubIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 2.247a10 10 0 00-3.162 19.487c.5.088.687-.212.687-.475 0-.237-.012-1.025-.012-1.862-2.513.462-3.163-.613-3.363-1.175a3.636 3.636 0 00-1.025-1.413c-.35-.187-.85-.65-.013-.662a2.001 2.001 0 011.538 1.025 2.137 2.137 0 002.912.825 2.104 2.104 0 01.638-1.338c-2.225-.25-4.55-1.112-4.55-4.937a3.892 3.892 0 011.025-2.688 3.594 3.594 0 01.1-2.65s.837-.262 2.75 1.025a9.427 9.427 0 015 0c1.912-1.3 2.75-1.025 2.75-1.025a3.593 3.593 0 01.1 2.65 3.869 3.869 0 011.025 2.688c0 3.837-2.338 4.687-4.563 4.937a2.368 2.368 0 01.675 1.85c0 1.338-.012 2.413-.012 2.75 0 .263.187.575.687.475A10.005 10.005 0 0012 2.247z' />
</IconSVG>
);
}
export function UpdateIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M2 12h2a7.986 7.986 0 012.337-5.663 7.91 7.91 0 012.542-1.71 8.12 8.12 0 016.13-.041A2.488 2.488 0 0017.5 7C18.886 7 20 5.886 20 4.5S18.886 2 17.5 2c-.689 0-1.312.276-1.763.725-2.431-.973-5.223-.958-7.635.059a9.928 9.928 0 00-3.18 2.139 9.92 9.92 0 00-2.14 3.179A10.005 10.005 0 002 12zm17.373 3.122c-.401.952-.977 1.808-1.71 2.541s-1.589 1.309-2.542 1.71a8.12 8.12 0 01-6.13.041A2.488 2.488 0 006.5 17C5.114 17 4 18.114 4 19.5S5.114 22 6.5 22c.689 0 1.312-.276 1.763-.725A9.965 9.965 0 0012 22a9.983 9.983 0 009.217-6.102A9.992 9.992 0 0022 12h-2a7.993 7.993 0 01-.627 3.122z' />
<path d='M12 7.462c-2.502 0-4.538 2.036-4.538 4.538S9.498 16.538 12 16.538s4.538-2.036 4.538-4.538S14.502 7.462 12 7.462zm0 7.076c-1.399 0-2.538-1.139-2.538-2.538S10.601 9.462 12 9.462s2.538 1.139 2.538 2.538-1.139 2.538-2.538 2.538z' />
</IconSVG>
);
}
export function InDoorIcon(props: IconProps) { export function InDoorIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 24 24' {...props}> <IconSVG viewbox='0 0 24 24' {...props}>
@ -401,38 +150,6 @@ export function InDoorIcon(props: IconProps) {
); );
} }
export function GotoLastIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M7.707 17.707L13.414 12 7.707 6.293 6.293 7.707 10.586 12l-4.293 4.293zM15 6h2v12h-2z' />
</IconSVG>
);
}
export function GotoFirstIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M16.293 17.707l1.414-1.414L13.414 12l4.293-4.293-1.414-1.414L10.586 12zM7 6h2v12H7z' />
</IconSVG>
);
}
export function GotoNextIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M10.707 17.707L16.414 12l-5.707-5.707-1.414 1.414L13.586 12l-4.293 4.293z' />
</IconSVG>
);
}
export function GotoPrevIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M13.293 6.293L7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z' />
</IconSVG>
);
}
export function DescendingIcon(props: IconProps) { export function DescendingIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 24 24' {...props}> <IconSVG viewbox='0 0 24 24' {...props}>
@ -472,61 +189,3 @@ export function CheckboxNullIcon() {
</svg> </svg>
); );
} }
export function ChevronUpIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M6.293 13.293l1.414 1.414L12 10.414l4.293 4.293 1.414-1.414L12 7.586z' />
</IconSVG>
);
}
export function ChevronDoubleUpIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M6.293 11.293l1.414 1.414L12 8.414l4.293 4.293 1.414-1.414L12 5.586z' />
<path d='M6.293 16.293l1.414 1.414L12 13.414l4.293 4.293 1.414-1.414L12 10.586z' />
</IconSVG>
);
}
export function ChevronDoubleDownIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 15.586l-4.293-4.293-1.414 1.414L12 18.414l5.707-5.707-1.414-1.414z' />
<path d='M17.707 7.707l-1.414-1.414L12 10.586 7.707 6.293 6.293 7.707 12 13.414z' />
</IconSVG>
);
}
export function CheckIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M10 15.586l-3.293-3.293-1.414 1.414L10 18.414l9.707-9.707-1.414-1.414z' />
</IconSVG>
);
}
export function CogIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 16c2.206 0 4-1.794 4-4s-1.794-4-4-4-4 1.794-4 4 1.794 4 4 4zm0-6c1.084 0 2 .916 2 2s-.916 2-2 2-2-.916-2-2 .916-2 2-2z' />
<path d='M2.845 16.136l1 1.73c.531.917 1.809 1.261 2.73.73l.529-.306A8.1 8.1 0 009 19.402V20c0 1.103.897 2 2 2h2c1.103 0 2-.897 2-2v-.598a8.132 8.132 0 001.896-1.111l.529.306c.923.53 2.198.188 2.731-.731l.999-1.729a2.001 2.001 0 00-.731-2.732l-.505-.292a7.718 7.718 0 000-2.224l.505-.292a2.002 2.002 0 00.731-2.732l-.999-1.729c-.531-.92-1.808-1.265-2.731-.732l-.529.306A8.1 8.1 0 0015 4.598V4c0-1.103-.897-2-2-2h-2c-1.103 0-2 .897-2 2v.598a8.132 8.132 0 00-1.896 1.111l-.529-.306c-.924-.531-2.2-.187-2.731.732l-.999 1.729a2.001 2.001 0 00.731 2.732l.505.292a7.683 7.683 0 000 2.223l-.505.292a2.003 2.003 0 00-.731 2.733zm3.326-2.758A5.703 5.703 0 016 12c0-.462.058-.926.17-1.378a.999.999 0 00-.47-1.108l-1.123-.65.998-1.729 1.145.662a.997.997 0 001.188-.142 6.071 6.071 0 012.384-1.399A1 1 0 0011 5.3V4h2v1.3a1 1 0 00.708.956 6.083 6.083 0 012.384 1.399.999.999 0 001.188.142l1.144-.661 1 1.729-1.124.649a1 1 0 00-.47 1.108c.112.452.17.916.17 1.378 0 .461-.058.925-.171 1.378a1 1 0 00.471 1.108l1.123.649-.998 1.729-1.145-.661a.996.996 0 00-1.188.142 6.071 6.071 0 01-2.384 1.399A1 1 0 0013 18.7l.002 1.3H11v-1.3a1 1 0 00-.708-.956 6.083 6.083 0 01-2.384-1.399.992.992 0 00-1.188-.141l-1.144.662-1-1.729 1.124-.651a1 1 0 00.471-1.108z' />
</IconSVG>
);
}
export function CrossIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 21 21' {...props}>
<g
fillRule='evenodd'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M15.5 15.5l-10-10zM15.5 5.5l-10 10' />
</g>
</IconSVG>
);
}

View File

@ -4,7 +4,7 @@ function Logo() {
const { darkMode } = useConceptTheme(); const { darkMode } = useConceptTheme();
return ( return (
<img alt='Логотип КонцептПортал' <img alt='Логотип КонцептПортал'
className='max-h-[1.6rem] min-w-[11rem]' className='max-h-[1.6rem] min-w-[11.5rem]'
src={!darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'} src={!darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
/>); />);
} }

View File

@ -29,7 +29,7 @@ function Navigation () {
{!noNavigation ? {!noNavigation ?
<div <div
className={clsx( className={clsx(
'pl-2 pr-[0.8rem] h-[3rem]', 'pl-2 pr-[0.9rem] h-[3rem]',
'flex justify-between', 'flex justify-between',
'border-b-2 rounded-none' 'border-b-2 rounded-none'
)} )}

View File

@ -1,25 +0,0 @@
import { DarkThemeIcon, LightThemeIcon } from '@/components/Icons';
import { useConceptTheme } from '@/context/ThemeContext';
import NavigationButton from './NavigationButton';
function ThemeSwitcher() {
const { darkMode, toggleDarkMode } = useConceptTheme();
if (darkMode) {
return (
<NavigationButton
description='Светлая тема'
icon={<LightThemeIcon />}
onClick={toggleDarkMode}
/>);
} else {
return (
<NavigationButton
description='Темная тема'
icon={<DarkThemeIcon />}
onClick={toggleDarkMode}
/>);
}
}
export default ThemeSwitcher;

View File

@ -27,20 +27,20 @@ function UserDropdown({ hideDropdown }: UserDropdownProps) {
return ( return (
<Dropdown dimensions='w-36' stretchLeft> <Dropdown dimensions='w-36' stretchLeft>
<DropdownButton <DropdownButton
text={user?.username}
tooltip='Профиль пользователя' tooltip='Профиль пользователя'
onClick={navigateProfile} onClick={navigateProfile}
> />
{user?.username}
</DropdownButton>
<DropdownButton <DropdownButton
text={darkMode ? 'Светлая тема' : 'Темная тема'}
tooltip='Переключение темы оформления' tooltip='Переключение темы оформления'
onClick={toggleDarkMode} onClick={toggleDarkMode}
> />
{darkMode ? 'Светлая тема' : 'Темная тема'} <DropdownButton
</DropdownButton> text='Выйти...'
<DropdownButton onClick={logoutAndRedirect}> className='font-semibold'
<b>Выйти...</b> onClick={logoutAndRedirect}
</DropdownButton> />
</Dropdown>); </Dropdown>);
} }

View File

@ -9,7 +9,7 @@ extends React.HTMLAttributes<HTMLDivElement> {
function InfoConstituenta({ data, ...restProps }: InfoConstituentaProps) { function InfoConstituenta({ data, ...restProps }: InfoConstituentaProps) {
return ( return (
<div {...restProps}> <div {...restProps}>
<h1>Конституента {data.alias}</h1> <h2>Конституента {data.alias}</h2>
<p> <p>
<b>Типизация: </b> <b>Типизация: </b>
{labelCstTypification(data)} {labelCstTypification(data)}

View File

@ -0,0 +1,36 @@
'use client';
import { createContext, useContext, useState } from 'react';
import { UserAccessMode } from '@/models/miscelanious';
interface IAccessModeContext {
mode: UserAccessMode
setMode: React.Dispatch<React.SetStateAction<UserAccessMode>>
}
const AccessContext = createContext<IAccessModeContext | null>(null);
export const useAccessMode = () => {
const context = useContext(AccessContext);
if (!context) {
throw new Error(
'useAccessMode has to be used within <AccessModeState.Provider>'
);
}
return context;
}
interface AccessModeStateProps {
children: React.ReactNode
}
export const AccessModeState = ({ children }: AccessModeStateProps) => {
const [mode, setMode] = useState<UserAccessMode>(UserAccessMode.READER);
return (
<AccessContext.Provider
value={{ mode, setMode }}
>
{children}
</AccessContext.Provider>);
};

View File

@ -30,15 +30,9 @@ interface IRSFormContext {
loading: boolean loading: boolean
processing: boolean processing: boolean
isMutable: boolean
isOwned: boolean isOwned: boolean
isClaimable: boolean isClaimable: boolean
isTracking: boolean isSubscribed: boolean
adminMode: boolean
toggleAdminMode: () => void
readerMode: boolean
toggleReaderMode: () => void
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void
claim: (callback?: DataCallback<ILibraryItem>) => void claim: (callback?: DataCallback<ILibraryItem>) => void
@ -76,8 +70,6 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID }); const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID });
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [adminMode, setAdminMode] = useState(false);
const [readerMode, setReaderMode] = useState(false);
const [toggleTracking, setToggleTracking] = useState(false); const [toggleTracking, setToggleTracking] = useState(false);
const isOwned = useMemo( const isOwned = useMemo(
@ -90,15 +82,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
return (user?.id !== schema?.owner && schema?.is_common && !schema?.is_canonical) ?? false; return (user?.id !== schema?.owner && schema?.is_common && !schema?.is_canonical) ?? false;
}, [user, schema?.owner, schema?.is_common, schema?.is_canonical]); }, [user, schema?.owner, schema?.is_common, schema?.is_canonical]);
const isMutable = useMemo( const isSubscribed = useMemo(
() => {
return (
!loading && !processing && !readerMode &&
((isOwned || (adminMode && user?.is_staff)) ?? false)
);
}, [user?.is_staff, readerMode, adminMode, isOwned, loading, processing]);
const isTracking = useMemo(
() => { () => {
if (!user || !schema || !user.id) { if (!user || !schema || !user.id) {
return false; return false;
@ -324,12 +308,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
<RSFormContext.Provider value={{ <RSFormContext.Provider value={{
schema, schema,
error, loading, processing, error, loading, processing,
adminMode, readerMode, isOwned, isMutable, isOwned,
isClaimable, isTracking, isClaimable, isSubscribed,
update, download, upload, claim, resetAliases, subscribe, unsubscribe, update, download, upload, claim, resetAliases, subscribe, unsubscribe,
cstUpdate, cstCreate, cstRename, cstDelete, cstMoveTo, cstUpdate, cstCreate, cstRename, cstDelete, cstMoveTo,
toggleAdminMode: () => setAdminMode(prev => !prev),
toggleReaderMode: () => setReaderMode(prev => !prev)
}}> }}>
{ children } { children }
</RSFormContext.Provider>); </RSFormContext.Provider>);

View File

@ -3,10 +3,10 @@
import { createColumnHelper } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
import { Dispatch, useCallback, useEffect, useMemo, useState } from 'react'; import { Dispatch, useCallback, useEffect, useMemo, useState } from 'react';
import { BiCheck, BiRefresh, BiX } from 'react-icons/bi';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import DataTable, { IConditionalStyle } from '@/components/DataTable'; import DataTable, { IConditionalStyle } from '@/components/DataTable';
import { ArrowsRotateIcon, CheckIcon, CrossIcon } from '@/components/Icons';
import RSInput from '@/components/RSInput'; import RSInput from '@/components/RSInput';
import ConstituentaPicker from '@/components/Shared/ConstituentaPicker'; import ConstituentaPicker from '@/components/Shared/ConstituentaPicker';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
@ -133,7 +133,7 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
{props.row.original.value ? {props.row.original.value ?
<MiniButton <MiniButton
tooltip='Очистить значение' tooltip='Очистить значение'
icon={<CrossIcon size={3} color='clr-text-warning'/>} icon={<BiX size='0.75rem' className='clr-text-warning'/>}
noHover noHover
onClick={() => handleClearArgument(props.row.original)} onClick={() => handleClearArgument(props.row.original)}
/> : null} /> : null}
@ -192,7 +192,7 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
<div className='flex'> <div className='flex'>
<MiniButton <MiniButton
tooltip='Подставить значение аргумента' tooltip='Подставить значение аргумента'
icon={<CheckIcon size={5} color={!argumentValue || !selectedArgument ? 'text-disabled' : 'clr-text-success'} />} icon={<BiCheck size='1.25rem' className={!argumentValue || !selectedArgument ? 'text-disabled' : 'clr-text-success'} />}
disabled={!argumentValue || !selectedArgument} disabled={!argumentValue || !selectedArgument}
onClick={() => handleAssignArgument(selectedArgument!, argumentValue)} onClick={() => handleAssignArgument(selectedArgument!, argumentValue)}
/> />
@ -200,12 +200,12 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
tooltip='Откатить значение' tooltip='Откатить значение'
disabled={!isModified} disabled={!isModified}
onClick={handleReset} onClick={handleReset}
icon={<ArrowsRotateIcon size={5} color={isModified ? 'clr-text-primary' : ''} />} icon={<BiRefresh size='1.25rem' className={isModified ? 'clr-text-primary' : ''} />}
/> />
<MiniButton <MiniButton
tooltip='Очистить значение аргумента' tooltip='Очистить значение аргумента'
disabled={!selectedClearable} disabled={!selectedClearable}
icon={<CrossIcon size={5} color={!selectedClearable ? 'text-disabled' : 'clr-text-warning'}/>} icon={<BiX size='1.25rem' className={!selectedClearable ? 'text-disabled' : 'clr-text-warning'}/>}
onClick={() => selectedArgument ? handleClearArgument(selectedArgument) : undefined} onClick={() => selectedArgument ? handleClearArgument(selectedArgument) : undefined}
/> />
</div> </div>

View File

@ -2,6 +2,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useLayoutEffect, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import { BiCheck, BiChevronsDown } from 'react-icons/bi';
import Label from '@/components/Common/Label'; import Label from '@/components/Common/Label';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
@ -9,7 +10,7 @@ import Modal from '@/components/Common/Modal';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import TextArea from '@/components/Common/TextArea'; import TextArea from '@/components/Common/TextArea';
import HelpButton from '@/components/Help/HelpButton'; import HelpButton from '@/components/Help/HelpButton';
import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, ChevronDoubleDownIcon } from '@/components/Icons'; import { ArrowLeftIcon, ArrowRightIcon } from '@/components/Icons';
import SelectGrammeme from '@/components/Shared/SelectGrammeme'; import SelectGrammeme from '@/components/Shared/SelectGrammeme';
import useConceptText from '@/hooks/useConceptText'; import useConceptText from '@/hooks/useConceptText';
import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '@/models/language'; import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '@/models/language';
@ -152,15 +153,15 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<div className='max-w-min'> <div className='max-w-min'>
<MiniButton <MiniButton
tooltip='Генерировать словоформу' tooltip='Генерировать словоформу'
icon={<ArrowLeftIcon size={5} color={inputGrams.length == 0 ? 'text-disabled' : 'clr-text-primary'} />} icon={<ArrowLeftIcon size='1.25rem' className={inputGrams.length == 0 ? 'text-disabled' : 'clr-text-primary'} />}
disabled={textProcessor.loading || inputGrams.length == 0} disabled={textProcessor.loading || inputGrams.length == 0}
onClick={handleInflect} onClick={handleInflect}
/> />
<MiniButton <MiniButton
tooltip='Определить граммемы' tooltip='Определить граммемы'
icon={<ArrowRightIcon icon={<ArrowRightIcon
size={5} size='1.25rem'
color={!inputText ? 'text-disabled' : 'clr-text-primary'} className={!inputText ? 'text-disabled' : 'clr-text-primary'}
/>} />}
disabled={textProcessor.loading || !inputText} disabled={textProcessor.loading || !inputText}
onClick={handleParse} onClick={handleParse}
@ -179,18 +180,16 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<Overlay position='top-2 left-0'> <Overlay position='top-2 left-0'>
<MiniButton <MiniButton
tooltip='Внести словоформу' tooltip='Внести словоформу'
icon={<CheckIcon icon={<BiCheck
size={5} size='1.25rem'
color={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'clr-text-success'} className={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'clr-text-success'}
/>} />}
disabled={textProcessor.loading || !inputText || inputGrams.length == 0} disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
onClick={handleAddForm} onClick={handleAddForm}
/> />
<MiniButton <MiniButton
tooltip='Генерировать стандартные словоформы' tooltip='Генерировать стандартные словоформы'
icon={<ChevronDoubleDownIcon icon={<BiChevronsDown size='1.25rem' className={!inputText ? 'text-disabled' : 'clr-text-primary'}
size={5}
color={!inputText ? 'text-disabled' : 'clr-text-primary'}
/>} />}
disabled={textProcessor.loading || !inputText} disabled={textProcessor.loading || !inputText}
onClick={handleGenerateLexeme} onClick={handleGenerateLexeme}

View File

@ -1,11 +1,11 @@
'use client'; 'use client';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { BiX } from 'react-icons/bi';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import DataTable, { createColumnHelper } from '@/components/DataTable'; import DataTable, { createColumnHelper } from '@/components/DataTable';
import { CrossIcon } from '@/components/Icons';
import WordFormBadge from '@/components/Shared/WordFormBadge'; import WordFormBadge from '@/components/Shared/WordFormBadge';
import { IWordForm } from '@/models/language'; import { IWordForm } from '@/models/language';
@ -68,7 +68,7 @@ function WordFormsTable({ forms, setForms, onFormSelect, loading }: WordFormsTab
cell: props => cell: props =>
<MiniButton noHover <MiniButton noHover
tooltip='Удалить словоформу' tooltip='Удалить словоформу'
icon={<CrossIcon size={4} color='text-warning'/>} icon={<BiX size='1rem' className='text-warning'/>}
onClick={() => handleDeleteRow(props.row.index)} onClick={() => handleDeleteRow(props.row.index)}
/> />
}) })
@ -79,7 +79,7 @@ function WordFormsTable({ forms, setForms, onFormSelect, loading }: WordFormsTab
<Overlay position='top-1 right-4'> <Overlay position='top-1 right-4'>
<MiniButton <MiniButton
tooltip='Сбросить все словоформы' tooltip='Сбросить все словоформы'
icon={<CrossIcon size={4} color={forms.length === 0 ? 'text-disabled' : 'text-warning'} />} icon={<BiX size='1rem' className={forms.length === 0 ? 'text-disabled' : 'text-warning'} />}
disabled={loading || forms.length === 0} disabled={loading || forms.length === 0}
onClick={handleResetAll} onClick={handleResetAll}
/> />

View File

@ -145,6 +145,10 @@
@apply text-lg font-semibold text-center @apply text-lg font-semibold text-center
} }
h2 {
@apply font-semibold text-center
}
b { b {
@apply font-semibold @apply font-semibold
} }

View File

@ -2,6 +2,15 @@
* Module: Miscellanious frontend model types. Future tagets for refactoring aimed at extracting modules. * Module: Miscellanious frontend model types. Future tagets for refactoring aimed at extracting modules.
*/ */
/**
* Represents user access mode.
*/
export enum UserAccessMode {
READER = 0,
OWNER,
ADMIN
}
/** /**
* Represents graph dependency mode. * Represents graph dependency mode.
*/ */

View File

@ -2,6 +2,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { BiDownload } from 'react-icons/bi';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Button from '@/components/Common/Button'; import Button from '@/components/Common/Button';
@ -12,7 +13,6 @@ import Overlay from '@/components/Common/Overlay';
import SubmitButton from '@/components/Common/SubmitButton'; import SubmitButton from '@/components/Common/SubmitButton';
import TextArea from '@/components/Common/TextArea'; import TextArea from '@/components/Common/TextArea';
import TextInput from '@/components/Common/TextInput'; import TextInput from '@/components/Common/TextInput';
import { DownloadIcon } from '@/components/Icons';
import InfoError from '@/components/InfoError'; import InfoError from '@/components/InfoError';
import RequireAuth from '@/components/RequireAuth'; import RequireAuth from '@/components/RequireAuth';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
@ -95,7 +95,7 @@ function CreateRSFormPage() {
/> />
<MiniButton <MiniButton
tooltip='Загрузить из Экстеор' tooltip='Загрузить из Экстеор'
icon={<DownloadIcon size={5} color='clr-text-primary'/>} icon={<BiDownload size='1.25rem' className='clr-text-primary'/>}
onClick={() => inputRef.current?.click()} onClick={() => inputRef.current?.click()}
/> />
</Overlay> </Overlay>

View File

@ -20,15 +20,15 @@ function ItemIcons({ user, item }: ItemIconsProps) {
> >
{(user && user.subscriptions.includes(item.id)) ? {(user && user.subscriptions.includes(item.id)) ?
<span title='Отслеживаемая'> <span title='Отслеживаемая'>
<SubscribedIcon size={3} /> <SubscribedIcon size='0.75rem' />
</span> : null} </span> : null}
{item.is_common ? {item.is_common ?
<span title='Общедоступная'> <span title='Общедоступная'>
<GroupIcon size={3}/> <GroupIcon size='0.75rem'/>
</span> : null} </span> : null}
{item.is_canonical ? {item.is_canonical ?
<span title='Неизменная'> <span title='Неизменная'>
<EducationIcon size={3}/> <EducationIcon size='0.75rem'/>
</span> : null} </span> : null}
</div>); </div>);
} }

View File

@ -1,11 +1,11 @@
'use client'; 'use client';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { BiFilterAlt } from 'react-icons/bi';
import Dropdown from '@/components/Common/Dropdown'; import Dropdown from '@/components/Common/Dropdown';
import DropdownCheckbox from '@/components/Common/DropdownCheckbox'; import DropdownCheckbox from '@/components/Common/DropdownCheckbox';
import SelectorButton from '@/components/Common/SelectorButton'; import SelectorButton from '@/components/Common/SelectorButton';
import { FilterIcon } from '@/components/Icons';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { LibraryFilterStrategy } from '@/models/miscelanious'; import { LibraryFilterStrategy } from '@/models/miscelanious';
@ -44,7 +44,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
<SelectorButton transparent tabIndex={-1} <SelectorButton transparent tabIndex={-1}
tooltip='Список фильтров' tooltip='Список фильтров'
dimensions='w-fit h-full' dimensions='w-fit h-full'
icon={<FilterIcon size={5} />} icon={<BiFilterAlt size='1.25rem' />}
text={labelLibraryFilter(value)} text={labelLibraryFilter(value)}
onClick={strategyMenu.toggle} onClick={strategyMenu.toggle}
/> />

View File

@ -1,13 +1,12 @@
'use client'; 'use client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { BiDiamond, BiDuplicate, BiPlusCircle, BiReset, BiTrash } from 'react-icons/bi';
import { FiSave } from "react-icons/fi";
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import HelpButton from '@/components/Help/HelpButton'; import HelpButton from '@/components/Help/HelpButton';
import {
ArrowsRotateIcon, CloneIcon, DiamondIcon, DumpBinIcon, SaveIcon, SmallPlusIcon
} from '@/components/Icons';
import { HelpTopic } from '@/models/miscelanious'; import { HelpTopic } from '@/models/miscelanious';
interface ConstituentaToolbarProps { interface ConstituentaToolbarProps {
@ -34,30 +33,30 @@ function ConstituentaToolbar({
<MiniButton <MiniButton
tooltip='Сохранить изменения [Ctrl + S]' tooltip='Сохранить изменения [Ctrl + S]'
disabled={!canSave} disabled={!canSave}
icon={<SaveIcon size={5} color={canSave ? 'clr-text-primary' : ''}/>} icon={<FiSave size='1.25rem' className={canSave ? 'clr-text-primary' : ''}/>}
onClick={onSubmit} onClick={onSubmit}
/> />
<MiniButton <MiniButton
tooltip='Сбросить несохраненные изменения' tooltip='Сбросить несохраненные изменения'
disabled={!canSave} disabled={!canSave}
onClick={onReset} onClick={onReset}
icon={<ArrowsRotateIcon size={5} color={canSave ? 'clr-text-primary' : ''} />} icon={<BiReset size='1.25rem' className={canSave ? 'clr-text-primary' : ''} />}
/> />
<MiniButton <MiniButton
tooltip='Создать конституенту после данной' tooltip='Создать конституенту после данной'
disabled={!isMutable} disabled={!isMutable}
onClick={onCreate} onClick={onCreate}
icon={<SmallPlusIcon size={5} color={isMutable ? 'clr-text-success' : ''} />} icon={<BiPlusCircle size={'1.25rem'} className={isMutable ? 'clr-text-success' : ''} />}
/> />
<MiniButton <MiniButton
tooltip='Клонировать конституенту [Alt + V]' tooltip='Клонировать конституенту [Alt + V]'
disabled={!isMutable} disabled={!isMutable}
onClick={onClone} onClick={onClone}
icon={<CloneIcon size={5} color={isMutable ? 'clr-text-success' : ''} />} icon={<BiDuplicate size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
/> />
<MiniButton <MiniButton
tooltip='Создать конституенту из шаблона [Alt + E]' tooltip='Создать конституенту из шаблона [Alt + E]'
icon={<DiamondIcon color={isMutable ? 'clr-text-primary': ''} size={5}/>} icon={<BiDiamond className={isMutable ? 'clr-text-primary': ''} size={'1.25rem'}/>}
disabled={!isMutable} disabled={!isMutable}
onClick={onTemplates} onClick={onTemplates}
/> />
@ -65,7 +64,7 @@ function ConstituentaToolbar({
tooltip='Удалить редактируемую конституенту' tooltip='Удалить редактируемую конституенту'
disabled={!isMutable} disabled={!isMutable}
onClick={onDelete} onClick={onDelete}
icon={<DumpBinIcon size={5} color={isMutable ? 'clr-text-warning' : ''} />} icon={<BiTrash size='1.25rem' className={isMutable ? 'clr-text-warning' : ''} />}
/> />
<HelpButton topic={HelpTopic.CONSTITUENTA} offset={4} /> <HelpButton topic={HelpTopic.CONSTITUENTA} offset={4} />
</Overlay>); </Overlay>);

View File

@ -18,6 +18,8 @@ const UNFOLDED_HEIGHT = '59.1rem';
const SIDELIST_HIDE_THRESHOLD = 1100; // px const SIDELIST_HIDE_THRESHOLD = 1100; // px
interface EditorConstituentaProps { interface EditorConstituentaProps {
isMutable: boolean
activeID?: number activeID?: number
activeCst?: IConstituenta | undefined activeCst?: IConstituenta | undefined
isModified: boolean isModified: boolean
@ -32,15 +34,15 @@ interface EditorConstituentaProps {
} }
function EditorConstituenta({ function EditorConstituenta({
isModified, setIsModified, activeID, activeCst, onEditTerm, isMutable, isModified, setIsModified, activeID, activeCst, onEditTerm,
onCreateCst, onRenameCst, onOpenEdit, onDeleteCst, onTemplates onCreateCst, onRenameCst, onOpenEdit, onDeleteCst, onTemplates
}: EditorConstituentaProps) { }: EditorConstituentaProps) {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const { schema, isMutable } = useRSForm(); const { schema } = useRSForm();
const [toggleReset, setToggleReset] = useState(false); const [toggleReset, setToggleReset] = useState(false);
const readyForEdit = useMemo(() => (!!activeCst && isMutable), [activeCst, isMutable]); const disabled = useMemo(() => (!activeCst || !isMutable), [activeCst, isMutable]);
function handleDelete() { function handleDelete() {
if (!schema || !activeID) { if (!schema || !activeID) {
@ -84,7 +86,7 @@ function EditorConstituenta({
} }
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) { function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
if (!isMutable) { if (disabled) {
return; return;
} }
if (event.ctrlKey && event.code === 'KeyS') { if (event.ctrlKey && event.code === 'KeyS') {
@ -120,7 +122,7 @@ function EditorConstituenta({
return (<> return (<>
<ConstituentaToolbar <ConstituentaToolbar
isMutable={readyForEdit} isMutable={!disabled}
isModified={isModified} isModified={isModified}
onSubmit={initiateSubmit} onSubmit={initiateSubmit}
@ -136,7 +138,8 @@ function EditorConstituenta({
onKeyDown={handleInput} onKeyDown={handleInput}
> >
<div className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'> <div className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'>
<FormConstituenta id={globalIDs.constituenta_editor} <FormConstituenta disabled={disabled}
id={globalIDs.constituenta_editor}
constituenta={activeCst} constituenta={activeCst}
isModified={isModified} isModified={isModified}
toggleReset={toggleReset} toggleReset={toggleReset}

View File

@ -1,13 +1,14 @@
'use client'; 'use client';
import { Dispatch, SetStateAction, useLayoutEffect, useMemo, useState } from 'react'; import { Dispatch, SetStateAction, useLayoutEffect, useState } from 'react';
import { FiSave } from 'react-icons/fi';
import { LiaEdit } from 'react-icons/lia';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import SubmitButton from '@/components/Common/SubmitButton'; import SubmitButton from '@/components/Common/SubmitButton';
import TextArea from '@/components/Common/TextArea'; import TextArea from '@/components/Common/TextArea';
import { EditIcon, SaveIcon } from '@/components/Icons';
import RefsInput from '@/components/RefsInput'; import RefsInput from '@/components/RefsInput';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import { IConstituenta, ICstRenameData, ICstUpdateData } from '@/models/rsform'; import { IConstituenta, ICstRenameData, ICstUpdateData } from '@/models/rsform';
@ -16,6 +17,8 @@ import { labelCstTypification } from '@/utils/labels';
import EditorRSExpression from '../EditorRSExpression'; import EditorRSExpression from '../EditorRSExpression';
interface FormConstituentaProps { interface FormConstituentaProps {
disabled?: boolean
id?: string id?: string
constituenta?: IConstituenta constituenta?: IConstituenta
@ -28,13 +31,12 @@ interface FormConstituentaProps {
} }
function FormConstituenta({ function FormConstituenta({
disabled,
id, isModified, setIsModified, id, isModified, setIsModified,
constituenta, toggleReset, constituenta, toggleReset,
onRenameCst, onEditTerm onRenameCst, onEditTerm
}: FormConstituentaProps) { }: FormConstituentaProps) {
const { schema, cstUpdate, isMutable, processing } = useRSForm(); const { schema, cstUpdate, processing } = useRSForm();
const readyForEdit = useMemo(() => (!!constituenta && isMutable), [constituenta, isMutable]);
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
const [term, setTerm] = useState(''); const [term, setTerm] = useState('');
@ -106,10 +108,10 @@ function FormConstituenta({
<Overlay position='top-0 left-[3rem]' className='flex justify-start select-none' > <Overlay position='top-0 left-[3rem]' className='flex justify-start select-none' >
<MiniButton <MiniButton
tooltip={`Редактировать словоформы термина: ${constituenta?.term_forms.length ?? 0}`} tooltip={`Редактировать словоформы термина: ${constituenta?.term_forms.length ?? 0}`}
disabled={!readyForEdit} disabled={disabled}
noHover noHover
onClick={onEditTerm} onClick={onEditTerm}
icon={<EditIcon size={4} color={readyForEdit ? 'clr-text-primary' : ''} />} icon={<LiaEdit size='1rem' className={!disabled ? 'clr-text-primary' : ''} />}
/> />
<div className='pt-1 pl-[1.375rem] text-sm font-semibold w-fit'> <div className='pt-1 pl-[1.375rem] text-sm font-semibold w-fit'>
<span>Имя </span> <span>Имя </span>
@ -117,9 +119,9 @@ function FormConstituenta({
</div> </div>
<MiniButton noHover <MiniButton noHover
tooltip='Переименовать конституенту' tooltip='Переименовать конституенту'
disabled={!readyForEdit} disabled={disabled}
onClick={handleRename} onClick={handleRename}
icon={<EditIcon size={4} color={readyForEdit ? 'clr-text-primary' : ''} />} icon={<LiaEdit size='1rem' className={!disabled ? 'clr-text-primary' : ''} />}
/> />
</Overlay> </Overlay>
<form id={id} <form id={id}
@ -133,7 +135,7 @@ function FormConstituenta({
value={term} value={term}
initialValue={constituenta?.term_raw ?? ''} initialValue={constituenta?.term_raw ?? ''}
resolved={constituenta?.term_resolved ?? ''} resolved={constituenta?.term_resolved ?? ''}
disabled={!readyForEdit} disabled={disabled}
onChange={newValue => setTerm(newValue)} onChange={newValue => setTerm(newValue)}
/> />
<TextArea dense noBorder <TextArea dense noBorder
@ -152,7 +154,7 @@ function FormConstituenta({
activeCst={constituenta} activeCst={constituenta}
placeholder='Родоструктурное выражение, задающее формальное определение' placeholder='Родоструктурное выражение, задающее формальное определение'
value={expression} value={expression}
disabled={!readyForEdit} disabled={disabled}
toggleReset={toggleReset} toggleReset={toggleReset}
onChange={newValue => setExpression(newValue)} onChange={newValue => setExpression(newValue)}
setTypification={setTypification} setTypification={setTypification}
@ -164,21 +166,21 @@ function FormConstituenta({
value={textDefinition} value={textDefinition}
initialValue={constituenta?.definition_raw ?? ''} initialValue={constituenta?.definition_raw ?? ''}
resolved={constituenta?.definition_resolved ?? ''} resolved={constituenta?.definition_resolved ?? ''}
disabled={!readyForEdit} disabled={disabled}
onChange={newValue => setTextDefinition(newValue)} onChange={newValue => setTextDefinition(newValue)}
/> />
<TextArea spellCheck <TextArea spellCheck
label='Конвенция / Комментарий' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение' placeholder='Договоренность об интерпретации или пояснение'
value={convention} value={convention}
disabled={!readyForEdit} disabled={disabled}
onChange={event => setConvention(event.target.value)} onChange={event => setConvention(event.target.value)}
/> />
<div className='flex justify-center w-full'> <div className='flex justify-center w-full'>
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
disabled={!isModified || !readyForEdit} disabled={!isModified || disabled}
icon={<SaveIcon size={6} />} icon={<FiSave size='1.5rem' />}
/> />
</div> </div>
</form> </form>

View File

@ -2,11 +2,11 @@
import { ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { useCallback, useLayoutEffect, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { PiGraphLight } from "react-icons/pi";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import { ASTNetworkIcon } from '@/components/Icons';
import RSInput from '@/components/RSInput'; import RSInput from '@/components/RSInput';
import { RSTextWrapper } from '@/components/RSInput/textEditing'; import { RSTextWrapper } from '@/components/RSInput/textEditing';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
@ -131,11 +131,11 @@ function EditorRSExpression({
syntaxTree={syntaxTree} syntaxTree={syntaxTree}
hideWindow={() => setShowAST(false)} hideWindow={() => setShowAST(false)}
/> : null} /> : null}
<Overlay position='top-[-0.2rem] left-[11rem]'> <Overlay position='top-[-0.375rem] left-[11rem]'>
<MiniButton noHover <MiniButton noHover
tooltip='Дерево разбора выражения' tooltip='Дерево разбора выражения'
onClick={handleShowAST} onClick={handleShowAST}
icon={<ASTNetworkIcon size={5} color='clr-text-primary' />} icon={<PiGraphLight size='1.25rem' className='clr-text-primary' />}
/> />
</Overlay> </Overlay>

View File

@ -13,16 +13,23 @@ import RSFormStats from './RSFormStats';
import RSFormToolbar from './RSFormToolbar'; import RSFormToolbar from './RSFormToolbar';
interface EditorRSFormProps { interface EditorRSFormProps {
isModified: boolean
isMutable: boolean
setIsModified: Dispatch<SetStateAction<boolean>>
onDestroy: () => void onDestroy: () => void
onClaim: () => void onClaim: () => void
onShare: () => void onShare: () => void
onDownload: () => void onDownload: () => void
isModified: boolean onToggleSubscribe: () => void
setIsModified: Dispatch<SetStateAction<boolean>>
} }
function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified, onDownload }: EditorRSFormProps) { function EditorRSForm({
const { schema, isMutable, isClaimable } = useRSForm(); isModified, isMutable,
onDestroy, onClaim, onShare, setIsModified,
onDownload, onToggleSubscribe
}: EditorRSFormProps) {
const { schema, isClaimable, isSubscribed, processing } = useRSForm();
const { user } = useAuth(); const { user } = useAuth();
function initiateSubmit() { function initiateSubmit() {
@ -45,6 +52,8 @@ function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified,
<div tabIndex={-1} onKeyDown={handleInput}> <div tabIndex={-1} onKeyDown={handleInput}>
<RSFormToolbar <RSFormToolbar
isMutable={isMutable} isMutable={isMutable}
processing={processing}
isSubscribed={isSubscribed}
modified={isModified} modified={isModified}
claimable={isClaimable} claimable={isClaimable}
anonymous={!user} anonymous={!user}
@ -54,11 +63,13 @@ function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified,
onDownload={onDownload} onDownload={onDownload}
onClaim={onClaim} onClaim={onClaim}
onDestroy={onDestroy} onDestroy={onDestroy}
onToggleSubscribe={onToggleSubscribe}
/> />
<div className='flex w-full'> <div className='flex w-full'>
<div className='flex-grow max-w-[40rem] min-w-[30rem] px-4 pb-2'> <div className='flex-grow max-w-[40rem] min-w-[30rem] px-4 pb-2'>
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
<FormRSForm id={globalIDs.library_item_editor} <FormRSForm disabled={!isMutable}
id={globalIDs.library_item_editor}
isModified={isModified} isModified={isModified}
setIsModified={setIsModified} setIsModified={setIsModified}
/> />

View File

@ -1,13 +1,14 @@
'use client'; 'use client';
import { Dispatch, SetStateAction, useLayoutEffect, useState } from 'react'; import { Dispatch, SetStateAction, useLayoutEffect, useState } from 'react';
import { FiSave } from 'react-icons/fi';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Checkbox from '@/components/Common/Checkbox'; import Checkbox from '@/components/Common/Checkbox';
import SubmitButton from '@/components/Common/SubmitButton'; import SubmitButton from '@/components/Common/SubmitButton';
import TextArea from '@/components/Common/TextArea'; import TextArea from '@/components/Common/TextArea';
import TextInput from '@/components/Common/TextInput'; import TextInput from '@/components/Common/TextInput';
import { SaveIcon } from '@/components/Icons'; import { useAuth } from '@/context/AuthContext';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import { LibraryItemType } from '@/models/library'; import { LibraryItemType } from '@/models/library';
import { IRSFormCreateData } from '@/models/rsform'; import { IRSFormCreateData } from '@/models/rsform';
@ -15,17 +16,16 @@ import { limits, patterns } from '@/utils/constants';
interface FormRSFormProps { interface FormRSFormProps {
id?: string id?: string
disabled: boolean
isModified: boolean isModified: boolean
setIsModified: Dispatch<SetStateAction<boolean>> setIsModified: Dispatch<SetStateAction<boolean>>
} }
function FormRSForm({ function FormRSForm({
id, isModified, setIsModified, id, disabled, isModified, setIsModified,
}: FormRSFormProps) { }: FormRSFormProps) {
const { const { schema, update, processing } = useRSForm();
schema, update, adminMode: adminMode, const { user } = useAuth();
isMutable: isMutable, processing
} = useRSForm();
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
@ -85,7 +85,7 @@ function FormRSForm({
<TextInput required <TextInput required
label='Полное название' label='Полное название'
value={title} value={title}
disabled={!isMutable} disabled={disabled}
onChange={event => setTitle(event.target.value)} onChange={event => setTitle(event.target.value)}
/> />
<TextInput required <TextInput required
@ -93,14 +93,14 @@ function FormRSForm({
dimensions='w-[14rem]' dimensions='w-[14rem]'
pattern={patterns.alias} pattern={patterns.alias}
tooltip={`не более ${limits.alias_len} символов`} tooltip={`не более ${limits.alias_len} символов`}
disabled={!isMutable} disabled={disabled}
value={alias} value={alias}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />
<TextArea <TextArea
label='Комментарий' label='Комментарий'
value={comment} value={comment}
disabled={!isMutable} disabled={disabled}
onChange={event => setComment(event.target.value)} onChange={event => setComment(event.target.value)}
/> />
<div className='flex justify-between whitespace-nowrap'> <div className='flex justify-between whitespace-nowrap'>
@ -108,7 +108,7 @@ function FormRSForm({
label='Общедоступная схема' label='Общедоступная схема'
tooltip='Общедоступные схемы видны всем пользователям и могут быть изменены' tooltip='Общедоступные схемы видны всем пользователям и могут быть изменены'
dimensions='w-fit' dimensions='w-fit'
disabled={!isMutable} disabled={disabled}
value={common} value={common}
setValue={value => setCommon(value)} setValue={value => setCommon(value)}
/> />
@ -116,7 +116,7 @@ function FormRSForm({
label='Неизменная схема' label='Неизменная схема'
tooltip='Только администраторы могут присваивать схемам неизменный статус' tooltip='Только администраторы могут присваивать схемам неизменный статус'
dimensions='w-fit' dimensions='w-fit'
disabled={!isMutable || !adminMode} disabled={disabled || !user?.is_staff}
value={canonical} value={canonical}
setValue={value => setCanonical(value)} setValue={value => setCanonical(value)}
/> />
@ -125,8 +125,8 @@ function FormRSForm({
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
loading={processing} loading={processing}
disabled={!isModified || !isMutable} disabled={!isModified || disabled}
icon={<SaveIcon size={6} />} icon={<FiSave size='1.5rem' />}
dimensions='my-2 w-fit' dimensions='my-2 w-fit'
/> />
</div> </div>

View File

@ -1,28 +1,35 @@
'use client'; 'use client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { BiDownload, BiTrash } from 'react-icons/bi';
import { FiSave } from 'react-icons/fi';
import { LuCrown } from 'react-icons/lu';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import HelpButton from '@/components/Help/HelpButton'; import HelpButton from '@/components/Help/HelpButton';
import { DownloadIcon, DumpBinIcon, OwnerIcon, SaveIcon, ShareIcon } from '@/components/Icons'; import { NotSubscribedIcon, ShareIcon, SubscribedIcon } from '@/components/Icons';
import { HelpTopic } from '@/models/miscelanious'; import { HelpTopic } from '@/models/miscelanious';
interface RSFormToolbarProps { interface RSFormToolbarProps {
isMutable: boolean isMutable: boolean
isSubscribed: boolean
modified: boolean modified: boolean
claimable: boolean claimable: boolean
anonymous: boolean anonymous: boolean
processing: boolean
onSubmit: () => void onSubmit: () => void
onShare: () => void onShare: () => void
onDownload: () => void onDownload: () => void
onClaim: () => void onClaim: () => void
onDestroy: () => void onDestroy: () => void
onToggleSubscribe: () => void
} }
function RSFormToolbar({ function RSFormToolbar({
isMutable, modified, claimable, anonymous, isMutable, modified, claimable, anonymous,
isSubscribed, onToggleSubscribe, processing,
onSubmit, onShare, onDownload, onSubmit, onShare, onDownload,
onClaim, onDestroy onClaim, onDestroy
}: RSFormToolbarProps) { }: RSFormToolbarProps) {
@ -32,30 +39,41 @@ function RSFormToolbar({
<MiniButton <MiniButton
tooltip='Сохранить изменения [Ctrl + S]' tooltip='Сохранить изменения [Ctrl + S]'
disabled={!canSave} disabled={!canSave}
icon={<SaveIcon size={5} color={canSave ? 'clr-text-primary' : ''}/>} icon={<FiSave size='1.25rem' className={canSave ? 'clr-text-primary' : ''}/>}
onClick={onSubmit} onClick={onSubmit}
/> />
<MiniButton <MiniButton
tooltip='Поделиться схемой' tooltip='Поделиться схемой'
icon={<ShareIcon size={5} color='clr-text-primary'/>} icon={<ShareIcon size='1.25rem' className='clr-text-primary'/>}
onClick={onShare} onClick={onShare}
/> />
<MiniButton <MiniButton
tooltip='Скачать TRS файл' tooltip='Скачать TRS файл'
icon={<DownloadIcon size={5} color='clr-text-primary'/>} icon={<BiDownload size='1.25rem' className='clr-text-primary'/>}
onClick={onDownload} onClick={onDownload}
/> />
<MiniButton
tooltip={'отслеживание: ' + (isSubscribed ? '[включено]' : '[выключено]')}
disabled={anonymous || processing}
icon={isSubscribed
? <SubscribedIcon size='1.25rem' className='clr-text-primary' />
: <NotSubscribedIcon size='1.25rem' className='clr-text-controls' />
}
dimensions='h-full w-fit pr-2'
style={{outlineColor: 'transparent'}}
onClick={onToggleSubscribe}
/>
<MiniButton <MiniButton
tooltip={claimable ? 'Стать владельцем' : 'Невозможно стать владельцем' } tooltip={claimable ? 'Стать владельцем' : 'Невозможно стать владельцем' }
icon={<OwnerIcon size={5} color={!claimable ? '' : 'clr-text-success'}/>} icon={<LuCrown size='1.25rem' className={!claimable ? '' : 'clr-text-success'}/>}
disabled={!claimable || anonymous} disabled={!claimable || anonymous || processing}
onClick={onClaim} onClick={onClaim}
/> />
<MiniButton <MiniButton
tooltip='Удалить схему' tooltip='Удалить схему'
disabled={!isMutable} disabled={!isMutable}
onClick={onDestroy} onClick={onDestroy}
icon={<DumpBinIcon size={5} color={isMutable ? 'clr-text-warning' : ''} />} icon={<BiTrash size='1.25rem' className={isMutable ? 'clr-text-warning' : ''} />}
/> />
<HelpButton topic={HelpTopic.RSFORM} offset={4} /> <HelpButton topic={HelpTopic.RSFORM} offset={4} />
</Overlay>); </Overlay>);

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import { useLayoutEffect, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { type RowSelectionState } from '@/components/DataTable'; import { type RowSelectionState } from '@/components/DataTable';
import SelectedCounter from '@/components/Shared/SelectedCounter'; import SelectedCounter from '@/components/Shared/SelectedCounter';
@ -12,14 +11,20 @@ import RSListToolbar from './RSListToolbar';
import RSTable from './RSTable'; import RSTable from './RSTable';
interface EditorRSListProps { interface EditorRSListProps {
isMutable: boolean
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
onTemplates: (insertAfter?: number) => void onTemplates: (insertAfter?: number) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
onReindex: () => void
} }
function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: EditorRSListProps) { function EditorRSList({
const { schema, isMutable, cstMoveTo, resetAliases } = useRSForm(); isMutable,
onOpenEdit, onCreateCst,
onDeleteCst, onTemplates, onReindex
}: EditorRSListProps) {
const { schema, cstMoveTo } = useRSForm();
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<number[]>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
@ -107,11 +112,6 @@ function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edi
}); });
} }
// Generate new names for all constituents
function handleReindex() {
resetAliases(() => toast.success('Имена конституент обновлены'));
}
function handleCreateCst(type?: CstType) { function handleCreateCst(type?: CstType) {
if (!schema) { if (!schema) {
return; return;
@ -186,7 +186,7 @@ function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edi
switch (code) { switch (code) {
case 'Backquote': handleCreateCst(); return true; case 'Backquote': handleCreateCst(); return true;
case 'KeyE': onTemplates(); return true; case 'KeyE': onTemplates(); return true;
case 'KeyR': handleReindex(); return true; case 'KeyR': onReindex(); return true;
case 'Digit1': handleCreateCst(CstType.BASE); return true; case 'Digit1': handleCreateCst(CstType.BASE); return true;
case 'Digit2': handleCreateCst(CstType.STRUCTURED); return true; case 'Digit2': handleCreateCst(CstType.STRUCTURED); return true;
@ -214,7 +214,7 @@ function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edi
onCreate={handleCreateCst} onCreate={handleCreateCst}
onDelete={handleDelete} onDelete={handleDelete}
onTemplates={() => onTemplates(selected.length !== 0 ? selected[selected.length-1] : undefined)} onTemplates={() => onTemplates(selected.length !== 0 ? selected[selected.length-1] : undefined)}
onReindex={handleReindex} onReindex={onReindex}
/> />
<SelectedCounter <SelectedCounter
total={schema?.stats?.count_all ?? 0} total={schema?.stats?.count_all ?? 0}

View File

@ -1,13 +1,13 @@
'use client'; 'use client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { BiAnalyse, BiDiamond, BiDownArrowCircle, BiDownvote, BiDuplicate, BiPlusCircle, BiTrash, BiUpvote } from "react-icons/bi";
import Dropdown from '@/components/Common/Dropdown'; import Dropdown from '@/components/Common/Dropdown';
import DropdownButton from '@/components/Common/DropdownButton'; import DropdownButton from '@/components/Common/DropdownButton';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import HelpButton from '@/components/Help/HelpButton'; import HelpButton from '@/components/Help/HelpButton';
import { ArrowDownIcon, ArrowDropdownIcon, ArrowUpIcon, CloneIcon, DiamondIcon, DumpBinIcon, SmallPlusIcon, UpdateIcon } from '@/components/Icons';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { HelpTopic } from '@/models/miscelanious'; import { HelpTopic } from '@/models/miscelanious';
import { CstType } from '@/models/rsform'; import { CstType } from '@/models/rsform';
@ -40,25 +40,25 @@ function RSListToolbar({
<Overlay position='w-full top-1 flex items-start justify-center'> <Overlay position='w-full top-1 flex items-start justify-center'>
<MiniButton <MiniButton
tooltip='Переместить вверх [Alt + вверх]' tooltip='Переместить вверх [Alt + вверх]'
icon={<ArrowUpIcon size={5}/>} icon={<BiUpvote size='1.25rem'/>}
disabled={!isMutable || nothingSelected} disabled={!isMutable || nothingSelected}
onClick={onMoveUp} onClick={onMoveUp}
/> />
<MiniButton <MiniButton
tooltip='Переместить вниз [Alt + вниз]' tooltip='Переместить вниз [Alt + вниз]'
icon={<ArrowDownIcon size={5}/>} icon={<BiDownvote size='1.25rem'/>}
disabled={!isMutable || nothingSelected} disabled={!isMutable || nothingSelected}
onClick={onMoveDown} onClick={onMoveDown}
/> />
<MiniButton <MiniButton
tooltip='Клонировать конституенту [Alt + V]' tooltip='Клонировать конституенту [Alt + V]'
icon={<CloneIcon color={isMutable && selectedCount === 1 ? 'clr-text-success': ''} size={5}/>} icon={<BiDuplicate size='1.25rem' className={isMutable && selectedCount === 1 ? 'clr-text-success': ''} />}
disabled={!isMutable || selectedCount !== 1} disabled={!isMutable || selectedCount !== 1}
onClick={onClone} onClick={onClone}
/> />
<MiniButton <MiniButton
tooltip='Добавить новую конституенту... [Alt + `]' tooltip='Добавить новую конституенту... [Alt + `]'
icon={<SmallPlusIcon color={isMutable ? 'clr-text-success': ''} size={5}/>} icon={<BiPlusCircle size='1.25rem' className={isMutable ? 'clr-text-success': ''} />}
disabled={!isMutable} disabled={!isMutable}
onClick={() => onCreate()} onClick={() => onCreate()}
/> />
@ -66,42 +66,39 @@ function RSListToolbar({
<div> <div>
<MiniButton <MiniButton
tooltip='Добавить пустую конституенту' tooltip='Добавить пустую конституенту'
icon={<ArrowDropdownIcon color={isMutable ? 'clr-text-success': ''} size={5}/>} icon={<BiDownArrowCircle size='1.25rem' className={isMutable ? 'clr-text-success': ''} />}
disabled={!isMutable} disabled={!isMutable}
onClick={insertMenu.toggle} onClick={insertMenu.toggle}
/> />
{insertMenu.isActive ? {insertMenu.isActive ?
<Dropdown> <Dropdown>
{(Object.values(CstType)).map( {(Object.values(CstType)).map(
(typeStr) => { (typeStr) =>
const type = typeStr as CstType;
return (
<DropdownButton <DropdownButton
key={`${prefixes.csttype_list}${typeStr}`} key={`${prefixes.csttype_list}${typeStr}`}
onClick={() => onCreate(type)} text={`${getCstTypePrefix(typeStr as CstType)}1 — ${labelCstType(typeStr as CstType)}`}
tooltip={getCstTypeShortcut(type)} onClick={() => onCreate(typeStr as CstType)}
> tooltip={getCstTypeShortcut(typeStr as CstType)}
{`${getCstTypePrefix(type)}1 — ${labelCstType(type)}`} />
</DropdownButton>); )}
})}
</Dropdown> : null} </Dropdown> : null}
</div> </div>
</div> </div>
<MiniButton <MiniButton
tooltip='Создать конституенту из шаблона [Alt + E]' tooltip='Создать конституенту из шаблона [Alt + E]'
icon={<DiamondIcon color={isMutable ? 'clr-text-primary': ''} size={5}/>} icon={<BiDiamond size='1.25rem' className={isMutable ? 'clr-text-primary': ''} />}
disabled={!isMutable} disabled={!isMutable}
onClick={onTemplates} onClick={onTemplates}
/> />
<MiniButton <MiniButton
tooltip='Сброс имен: присвоить порядковые имена [Alt + R]' tooltip='Сброс имён: присвоить порядковые имена [Alt + R]'
icon={<UpdateIcon color={isMutable ? 'clr-text-primary': ''} size={5}/>} icon={<BiAnalyse size='1.25rem' className={isMutable ? 'clr-text-primary': ''} />}
disabled={!isMutable} disabled={!isMutable}
onClick={onReindex} onClick={onReindex}
/> />
<MiniButton <MiniButton
tooltip='Удалить выбранные [Delete]' tooltip='Удалить выбранные [Delete]'
icon={<DumpBinIcon color={isMutable && !nothingSelected ? 'clr-text-warning' : ''} size={5}/>} icon={<BiTrash size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}
disabled={!isMutable || nothingSelected} disabled={!isMutable || nothingSelected}
onClick={onDelete} onClick={onDelete}
/> />

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import DataTable, { createColumnHelper,RowSelectionState,VisibilityState } from '@/components/DataTable'; import DataTable, { createColumnHelper,RowSelectionState,VisibilityState } from '@/components/DataTable';
@ -71,7 +72,6 @@ function RSTable({
theme={colors} theme={colors}
value={props.row.original} value={props.row.original}
prefixID={prefixes.cst_list} prefixID={prefixes.cst_list}
shortTooltip
/> />
}), }),
columnHelper.accessor(cst => labelCstTypification(cst), { columnHelper.accessor(cst => labelCstTypification(cst), {
@ -125,7 +125,15 @@ function RSTable({
}, [noNavigation]); }, [noNavigation]);
return ( return (
<div className='w-full h-full overflow-auto text-sm min-h-[20rem]' style={{maxHeight: tableHeight}}> <div
className={clsx(
'w-full h-full min-h-[20rem]',
'overflow-auto',
'text-sm',
'select-none'
)}
style={{maxHeight: tableHeight}}
>
<DataTable dense noFooter <DataTable dense noFooter
data={items ?? []} data={items ?? []}
columns={columns} columns={columns}

View File

@ -23,13 +23,14 @@ import useGraphFilter from './useGraphFilter';
import ViewHidden from './ViewHidden'; import ViewHidden from './ViewHidden';
interface EditorTermGraphProps { interface EditorTermGraphProps {
isMutable: boolean
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
} }
function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) { function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) {
const { schema, isMutable } = useRSForm(); const { schema } = useRSForm();
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
const [toggleDataUpdate, setToggleDataUpdate] = useState(false); const [toggleDataUpdate, setToggleDataUpdate] = useState(false);

View File

@ -1,9 +1,11 @@
'use client'; 'use client';
import { BiCollapse, BiFilterAlt, BiPlanet, BiPlusCircle, BiTrash } from 'react-icons/bi';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import HelpButton from '@/components/Help/HelpButton'; import HelpButton from '@/components/Help/HelpButton';
import { ArrowsFocusIcon, DumpBinIcon, FilterIcon, LetterAIcon, LetterALinesIcon, PlanetIcon, SmallPlusIcon } from '@/components/Icons'; import { LetterAIcon, LetterALinesIcon } from '@/components/Icons';
import { HelpTopic } from '@/models/miscelanious'; import { HelpTopic } from '@/models/miscelanious';
interface GraphToolbarProps { interface GraphToolbarProps {
@ -34,37 +36,37 @@ function GraphToolbar({
<Overlay position='w-full top-1 right-0 flex items-start justify-center'> <Overlay position='w-full top-1 right-0 flex items-start justify-center'>
<MiniButton <MiniButton
tooltip='Настройки фильтрации узлов и связей' tooltip='Настройки фильтрации узлов и связей'
icon={<FilterIcon color='clr-text-primary' size={5} />} icon={<BiFilterAlt size='1.25rem' className='clr-text-primary' />}
onClick={showParamsDialog} onClick={showParamsDialog}
/> />
<MiniButton <MiniButton
tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'} tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={ icon={
!noText !noText
? <LetterALinesIcon color='clr-text-success' size={5} /> ? <LetterALinesIcon size='1.25rem' className='clr-text-success' />
: <LetterAIcon color='clr-text-primary' size={5} /> : <LetterAIcon size='1.25rem' className='clr-text-primary' />
} }
onClick={toggleNoText} onClick={toggleNoText}
/> />
<MiniButton <MiniButton
tooltip='Новая конституента' tooltip='Новая конституента'
icon={<SmallPlusIcon color={isMutable ? 'clr-text-success' : ''} size={5} />} icon={<BiPlusCircle size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
disabled={!isMutable} disabled={!isMutable}
onClick={onCreate} onClick={onCreate}
/> />
<MiniButton <MiniButton
tooltip='Удалить выбранные' tooltip='Удалить выбранные'
icon={<DumpBinIcon color={isMutable && !nothingSelected ? 'clr-text-warning' : ''} size={5} />} icon={<BiTrash size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}
disabled={!isMutable || nothingSelected} disabled={!isMutable || nothingSelected}
onClick={onDelete} onClick={onDelete}
/> />
<MiniButton <MiniButton
icon={<ArrowsFocusIcon color='clr-text-primary' size={5} />} icon={<BiCollapse size='1.25rem' className='clr-text-primary' />}
tooltip='Восстановить камеру' tooltip='Восстановить камеру'
onClick={onResetViewpoint} onClick={onResetViewpoint}
/> />
<MiniButton <MiniButton
icon={<PlanetIcon color={!is3D ? '' : orbit ? 'clr-text-success' : 'clr-text-primary'} size={5} />} icon={<BiPlanet size='1.25rem' className={!is3D ? '' : orbit ? 'clr-text-success' : 'clr-text-primary'} />}
tooltip='Анимация вращения' tooltip='Анимация вращения'
disabled={!is3D} disabled={!is3D}
onClick={toggleOrbit} onClick={toggleOrbit}

View File

@ -2,6 +2,7 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { AccessModeState } from '@/context/AccessModeContext';
import { RSFormState } from '@/context/RSFormContext'; import { RSFormState } from '@/context/RSFormContext';
import RSTabs from './RSTabs'; import RSTabs from './RSTabs';
@ -9,9 +10,11 @@ import RSTabs from './RSTabs';
function RSFormPage() { function RSFormPage() {
const params = useParams(); const params = useParams();
return ( return (
<AccessModeState>
<RSFormState schemaID={params.id ?? ''}> <RSFormState schemaID={params.id ?? ''}>
<RSTabs /> <RSTabs />
</RSFormState>); </RSFormState>
</AccessModeState>);
} }
export default RSFormPage; export default RSFormPage;

View File

@ -11,6 +11,8 @@ import { ConceptLoader } from '@/components/Common/ConceptLoader';
import ConceptTab from '@/components/Common/ConceptTab'; import ConceptTab from '@/components/Common/ConceptTab';
import TextURL from '@/components/Common/TextURL'; import TextURL from '@/components/Common/TextURL';
import InfoError, { ErrorData } from '@/components/InfoError'; import InfoError, { ErrorData } from '@/components/InfoError';
import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { useBlockNavigation, useConceptNavigation } from '@/context/NagivationContext'; import { useBlockNavigation, useConceptNavigation } from '@/context/NagivationContext';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
@ -23,6 +25,7 @@ import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
import DlgRenameCst from '@/dialogs/DlgRenameCst'; import DlgRenameCst from '@/dialogs/DlgRenameCst';
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { UserAccessMode } from '@/models/miscelanious';
import { IConstituenta, ICstCreateData, ICstRenameData, ICstUpdateData, TermForm } from '@/models/rsform'; import { IConstituenta, ICstCreateData, ICstRenameData, ICstUpdateData, TermForm } from '@/models/rsform';
import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants'; import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants';
import { createAliasFor } from '@/utils/misc'; import { createAliasFor } from '@/utils/misc';
@ -56,20 +59,30 @@ function ProcessError({error}: {error: ErrorData}): React.ReactElement {
function RSTabs() { function RSTabs() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const tabQuery = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID; const activeTab = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID;
const cstQuery = query.get('active'); const cstQuery = query.get('active');
const { const {
error, schema, loading, claim, download, isTracking, error, schema, loading, processing, isOwned,
cstCreate, cstDelete, cstRename, subscribe, unsubscribe, cstUpdate claim, download, isSubscribed,
cstCreate, cstDelete, cstRename, subscribe, unsubscribe, cstUpdate, resetAliases
} = useRSForm(); } = useRSForm();
const { destroyItem } = useLibrary(); const { destroyItem } = useLibrary();
const { setNoFooter, noNavigation } = useConceptTheme(); const { setNoFooter, noNavigation } = useConceptTheme();
const { user } = useAuth();
const { mode, setMode } = useAccessMode();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
useBlockNavigation(isModified); useBlockNavigation(isModified);
const [activeTab, setActiveTab] = useState(RSTabID.CARD); const isMutable = useMemo(
() => {
return (
!loading && !processing && mode !== UserAccessMode.READER &&
((isOwned || (mode === UserAccessMode.ADMIN && user?.is_staff)) ?? false)
);
}, [user?.is_staff, mode, isOwned, loading, processing]);
const [activeID, setActiveID] = useState<number | undefined>(undefined); const [activeID, setActiveID] = useState<number | undefined>(undefined);
const activeCst = useMemo( const activeCst = useMemo(
() => schema?.items?.find(cst => cst.id === activeID) () => schema?.items?.find(cst => cst.id === activeID)
@ -111,12 +124,22 @@ function RSTabs() {
}, [schema, schema?.title]); }, [schema, schema?.title]);
useLayoutEffect(() => { useLayoutEffect(() => {
setActiveTab(tabQuery); setNoFooter(activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST);
setNoFooter(tabQuery === RSTabID.CST_EDIT || tabQuery === RSTabID.CST_LIST);
setActiveID(Number(cstQuery) ?? ((schema && schema?.items.length > 0) ? schema.items[0].id : undefined)); setActiveID(Number(cstQuery) ?? ((schema && schema?.items.length > 0) ? schema.items[0].id : undefined));
setIsModified(false); setIsModified(false);
return () => setNoFooter(false); return () => setNoFooter(false);
}, [tabQuery, cstQuery, setActiveTab, setActiveID, schema, setNoFooter, setIsModified]); }, [activeTab, cstQuery, setActiveID, schema, setNoFooter, setIsModified]);
useLayoutEffect(
() => setMode((prev) => {
if (prev === UserAccessMode.ADMIN) {
return prev;
} else if(isOwned) {
return UserAccessMode.OWNER;
} else {
return UserAccessMode.READER;
}
}), [schema, setMode, isOwned]);
function onSelectTab(index: number) { function onSelectTab(index: number) {
navigateTab(index, activeID); navigateTab(index, activeID);
@ -186,6 +209,11 @@ function RSTabs() {
setShowRenameCst(true); setShowRenameCst(true);
}, []); }, []);
const onReindex = useCallback(
() => resetAliases(
() => toast.success('Имена конституент обновлены')
), [resetAliases]);
const handleDeleteCst = useCallback( const handleDeleteCst = useCallback(
(deleted: number[]) => { (deleted: number[]) => {
if (!schema) { if (!schema) {
@ -290,12 +318,12 @@ function RSTabs() {
const handleToggleSubscribe = useCallback( const handleToggleSubscribe = useCallback(
() => { () => {
if (isTracking) { if (isSubscribed) {
unsubscribe(() => toast.success('Отслеживание отключено')); unsubscribe(() => toast.success('Отслеживание отключено'));
} else { } else {
subscribe(() => toast.success('Отслеживание включено')); subscribe(() => toast.success('Отслеживание включено'));
} }
}, [isTracking, subscribe, unsubscribe]); }, [isSubscribed, subscribe, unsubscribe]);
const promptShowEditTerm = useCallback( const promptShowEditTerm = useCallback(
() => { () => {
@ -383,12 +411,13 @@ function RSTabs() {
'flex justify-stretch', 'flex justify-stretch',
'border-b-2 border-x-2 divide-x-2' 'border-b-2 border-x-2 divide-x-2'
)}> )}>
<RSTabsMenu <RSTabsMenu isMutable={isMutable}
onTemplates={onShowTemplates}
onDownload={onDownloadSchema} onDownload={onDownloadSchema}
onDestroy={onDestroySchema} onDestroy={onDestroySchema}
onClaim={onClaimSchema} onClaim={onClaimSchema}
onShare={onShareSchema} onShare={onShareSchema}
onToggleSubscribe={handleToggleSubscribe} onReindex={onReindex}
showCloneDialog={promptClone} showCloneDialog={promptClone}
showUploadDialog={() => setShowUpload(true)} showUploadDialog={() => setShowUpload(true)}
/> />
@ -418,8 +447,10 @@ function RSTabs() {
> >
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}>
<EditorRSForm <EditorRSForm
isMutable={isMutable}
isModified={isModified} isModified={isModified}
setIsModified={setIsModified} setIsModified={setIsModified}
onToggleSubscribe={handleToggleSubscribe}
onDownload={onDownloadSchema} onDownload={onDownloadSchema}
onDestroy={onDestroySchema} onDestroy={onDestroySchema}
onClaim={onClaimSchema} onClaim={onClaimSchema}
@ -429,15 +460,18 @@ function RSTabs() {
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '': 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '': 'none' }}>
<EditorRSList <EditorRSList
isMutable={isMutable}
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst} onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst} onDeleteCst={promptDeleteCst}
onTemplates={onShowTemplates} onTemplates={onShowTemplates}
onReindex={onReindex}
/> />
</TabPanel> </TabPanel>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '': 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '': 'none' }}>
<EditorConstituenta <EditorConstituenta
isMutable={isMutable}
isModified={isModified} isModified={isModified}
setIsModified={setIsModified} setIsModified={setIsModified}
activeID={activeID} activeID={activeID}
@ -453,6 +487,7 @@ function RSTabs() {
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '': 'none' }}> <TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '': 'none' }}>
<EditorTermGraph <EditorTermGraph
isMutable={isMutable}
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst} onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst} onDeleteCst={promptDeleteCst}

View File

@ -1,19 +1,24 @@
'use client'; 'use client';
import { BiAnalyse, BiDiamond, BiDownload, BiDuplicate, BiMenu, BiMeteor, BiPlusCircle, BiTrash, BiUpload } from 'react-icons/bi';
import { FiEdit } from 'react-icons/fi';
import { LuCrown, LuGlasses } from 'react-icons/lu';
import Button from '@/components/Common/Button'; import Button from '@/components/Common/Button';
import Dropdown from '@/components/Common/Dropdown'; import Dropdown from '@/components/Common/Dropdown';
import DropdownButton from '@/components/Common/DropdownButton'; import DropdownButton from '@/components/Common/DropdownButton';
import DropdownCheckbox from '@/components/Common/DropdownCheckbox'; import { ShareIcon } from '@/components/Icons';
import { import { useAccessMode } from '@/context/AccessModeContext';
CloneIcon, DownloadIcon, DumpBinIcon, EditIcon, MenuIcon, NotSubscribedIcon,
OwnerIcon, ShareIcon, SmallPlusIcon, SubscribedIcon, UploadIcon
} from '@/components/Icons';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NagivationContext'; import { useConceptNavigation } from '@/context/NagivationContext';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { UserAccessMode } from '@/models/miscelanious';
import { describeAccessMode, labelAccessMode } from '@/utils/labels';
interface RSTabsMenuProps { interface RSTabsMenuProps {
isMutable: boolean
showUploadDialog: () => void showUploadDialog: () => void
showCloneDialog: () => void showCloneDialog: () => void
@ -21,21 +26,25 @@ interface RSTabsMenuProps {
onClaim: () => void onClaim: () => void
onShare: () => void onShare: () => void
onDownload: () => void onDownload: () => void
onToggleSubscribe: () => void onReindex: () => void
onTemplates: () => void
} }
function RSTabsMenu({ function RSTabsMenu({
isMutable,
showUploadDialog, showCloneDialog, showUploadDialog, showCloneDialog,
onDestroy, onShare, onDownload, onClaim, onToggleSubscribe onDestroy, onShare, onDownload,
onClaim, onReindex, onTemplates
}: RSTabsMenuProps) { }: RSTabsMenuProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const { const { isOwned, isClaimable } = useRSForm();
isOwned, isMutable, isTracking, readerMode, isClaimable, adminMode,
toggleAdminMode, toggleReaderMode, processing const { mode, setMode } = useAccessMode();
} = useRSForm();
const schemaMenu = useDropdown(); const schemaMenu = useDropdown();
const editMenu = useDropdown(); const editMenu = useDropdown();
const accessMenu = useDropdown();
function handleClaimOwner() { function handleClaimOwner() {
editMenu.hide(); editMenu.hide();
@ -67,6 +76,21 @@ function RSTabsMenu({
onShare(); onShare();
} }
function handleReindex() {
editMenu.hide();
onReindex();
}
function handleTemplates() {
editMenu.hide();
onTemplates();
}
function handleChangeMode(newMode: UserAccessMode) {
accessMenu.hide();
setMode(newMode);
}
function handleCreateNew() { function handleCreateNew() {
router.push('/rsform-create'); router.push('/rsform-create');
} }
@ -75,105 +99,111 @@ function RSTabsMenu({
<div className='flex items-stretch h-full w-fit'> <div className='flex items-stretch h-full w-fit'>
<div ref={schemaMenu.ref}> <div ref={schemaMenu.ref}>
<Button noBorder dense tabIndex={-1} <Button noBorder dense tabIndex={-1}
tooltip='Действия' tooltip='Меню'
icon={<MenuIcon color='clr-text-controls' size={5}/>} icon={<BiMenu size='1.25rem' className='clr-text-controls' />}
dimensions='h-full w-fit pl-2' dimensions='h-full w-fit pl-2'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
onClick={schemaMenu.toggle} onClick={schemaMenu.toggle}
/> />
{schemaMenu.isActive ? {schemaMenu.isActive ?
<Dropdown> <Dropdown>
<DropdownButton onClick={handleShare}> <DropdownButton
<div className='inline-flex items-center justify-start gap-2'> text={isOwned ? 'Вы — владелец' : 'Стать владельцем'}
<ShareIcon color='clr-text-primary' size={4}/> tooltip={!user || !isClaimable ? 'Взять во владение можно общую изменяемую схему' : ''}
<p>Поделиться</p> icon={<LuCrown size='1rem' className={isOwned ? 'clr-text-success' : 'clr-text-controls'} />}
</div> onClick={(!isOwned && user && isClaimable) ? handleClaimOwner : undefined}
</DropdownButton> />
<DropdownButton onClick={handleClone} disabled={!user} > <DropdownButton
<div className='inline-flex items-center justify-start gap-2'> text='Поделиться'
<CloneIcon color='clr-text-primary' size={4}/> icon={<ShareIcon size='1rem' className='clr-text-primary' />}
<p>Клонировать</p> onClick={handleShare}
</div> />
</DropdownButton> <DropdownButton disabled={!user}
<DropdownButton onClick={handleDownload}> text='Клонировать'
<div className='inline-flex items-center justify-start gap-2'> icon={<BiDuplicate size='1rem' className='clr-text-primary' />}
<DownloadIcon color='clr-text-primary' size={4}/> onClick={handleClone}
<p>Выгрузить в Экстеор</p> />
</div> <DropdownButton
</DropdownButton> text='Выгрузить в Экстеор'
<DropdownButton disabled={!isMutable} onClick={handleUpload}> icon={<BiDownload size='1rem' className='clr-text-primary'/>}
<div className='inline-flex items-center justify-start gap-2'> onClick={handleDownload}
<UploadIcon color={isMutable ? 'clr-text-warning' : ''} size={4}/> />
<p>Загрузить из Экстеора</p> <DropdownButton disabled={!isMutable}
</div> text='Загрузить из Экстеора'
</DropdownButton> icon={<BiUpload size='1rem' className={isMutable ? 'clr-text-warning' : ''} />}
<DropdownButton disabled={!isMutable} onClick={handleDelete}> onClick={handleUpload}
<span className='inline-flex items-center justify-start gap-2'> />
<DumpBinIcon color={isMutable ? 'clr-text-warning' : ''} size={4} /> <DropdownButton disabled={!isMutable}
<p>Удалить схему</p> text='Удалить схему'
</span> icon={<BiTrash size='1rem' className={isMutable ? 'clr-text-warning' : ''} />}
</DropdownButton> onClick={handleDelete}
<DropdownButton onClick={handleCreateNew}> />
<span className='inline-flex items-center justify-start gap-2'> <DropdownButton
<SmallPlusIcon color='clr-text-url' size={4} /> text='Создать новую схему'
<p>Создать новую схему</p> icon={<BiPlusCircle size='1rem' className='clr-text-url' />}
</span> onClick={handleCreateNew}
</DropdownButton> />
</Dropdown> : null} </Dropdown> : null}
</div> </div>
<div ref={editMenu.ref}> <div ref={editMenu.ref}>
<Button dense noBorder tabIndex={-1} <Button dense noBorder tabIndex={-1}
tooltip={'измнение: ' + (isMutable ? '[доступно]' : '[запрещено]')} tooltip={'Редактирование'}
dimensions='h-full w-fit' dimensions='h-full w-fit'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
icon={<EditIcon size={5} color={isMutable ? 'clr-text-success' : 'clr-text-warning'}/>} icon={<FiEdit size='1.25rem' className={isMutable ? 'clr-text-success' : 'clr-text-warning'}/>}
onClick={editMenu.toggle} onClick={editMenu.toggle}
/> />
{editMenu.isActive ? {editMenu.isActive ?
<Dropdown> <Dropdown>
<DropdownButton <DropdownButton disabled={!isMutable}
disabled={!user || !isClaimable} text='Сброс имён'
onClick={!isOwned ? handleClaimOwner : undefined} tooltip='Присвоить порядковые имена и обновить выражения'
tooltip={!user || !isClaimable ? 'Взять во владение можно общую изменяемую схему' : ''} icon={<BiAnalyse size='1rem' className={isMutable ? 'clr-text-primary': ''} />}
> onClick={handleReindex}
<div className='flex items-center gap-2 clr-text-default'> />
<span> <DropdownButton disabled={!isMutable}
<OwnerIcon size={4} color={isOwned ? 'clr-text-success' : 'clr-text-controls'} /> text='Банк выражений'
</span> tooltip='Создать конституенту из шаблона'
<div> icon={<BiDiamond size='1rem' className={isMutable ? 'clr-text-success': ''} />}
{isOwned ? <b className='clr-text-default'>Вы владелец</b> : null} onClick={handleTemplates}
{!isOwned ? <b>Стать владельцем</b> : null} />
</div>
</div>
</DropdownButton>
{(isOwned || user?.is_staff) ?
<DropdownCheckbox
value={readerMode}
setValue={toggleReaderMode}
label='Я — читатель!'
tooltip='Режим чтения'
/> : null}
{user?.is_staff ?
<DropdownCheckbox
value={adminMode}
setValue={toggleAdminMode}
label='Я — администратор!'
tooltip='Режим редактирования для администраторов'
/> : null}
</Dropdown>: null} </Dropdown>: null}
</div> </div>
<div>
<div ref={accessMenu.ref}>
<Button dense noBorder tabIndex={-1} <Button dense noBorder tabIndex={-1}
tooltip={'отслеживание: ' + (isTracking ? '[включено]' : '[выключено]')} tooltip={`режим ${labelAccessMode(mode)}`}
disabled={processing}
icon={isTracking
? <SubscribedIcon color='clr-text-primary' size={5}/>
: <NotSubscribedIcon color='clr-text-controls' size={5}/>
}
dimensions='h-full w-fit pr-2' dimensions='h-full w-fit pr-2'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
onClick={onToggleSubscribe} icon={
mode === UserAccessMode.ADMIN ? <BiMeteor size='1.25rem' className='clr-text-primary'/>
: mode === UserAccessMode.OWNER ? <LuCrown size='1.25rem' className='clr-text-primary'/>
: <LuGlasses size='1.25rem' className='clr-text-primary'/>
}
onClick={accessMenu.toggle}
/>
{accessMenu.isActive ?
<Dropdown>
<DropdownButton
text={labelAccessMode(UserAccessMode.READER)}
tooltip={describeAccessMode(UserAccessMode.READER)}
icon={<LuGlasses size='1rem' className='clr-text-primary' />}
onClick={() => handleChangeMode(UserAccessMode.READER)}
/> />
<DropdownButton disabled={!isOwned}
text={labelAccessMode(UserAccessMode.OWNER)}
tooltip={describeAccessMode(UserAccessMode.OWNER)}
icon={<LuCrown size='1rem' className={isOwned ? 'clr-text-primary': ''} />}
onClick={() => handleChangeMode(UserAccessMode.OWNER)}
/>
<DropdownButton disabled={!user?.is_staff}
text={labelAccessMode(UserAccessMode.ADMIN)}
tooltip={describeAccessMode(UserAccessMode.ADMIN)}
icon={<BiMeteor size='1rem' className={user?.is_staff ? 'clr-text-primary': ''} />}
onClick={() => handleChangeMode(UserAccessMode.ADMIN)}
/>
</Dropdown>: null}
</div> </div>
</div>); </div>);
} }

View File

@ -1,12 +1,12 @@
'use client'; 'use client';
import { useCallback, useLayoutEffect } from 'react'; import { useCallback, useLayoutEffect } from 'react';
import { BiCog, BiFilterAlt } from 'react-icons/bi';
import ConceptSearch from '@/components/Common/ConceptSearch'; import ConceptSearch from '@/components/Common/ConceptSearch';
import Dropdown from '@/components/Common/Dropdown'; import Dropdown from '@/components/Common/Dropdown';
import DropdownButton from '@/components/Common/DropdownButton'; import DropdownButton from '@/components/Common/DropdownButton';
import SelectorButton from '@/components/Common/SelectorButton'; import SelectorButton from '@/components/Common/SelectorButton';
import { CogIcon, FilterIcon } from '@/components/Icons';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { CstMatchMode, DependencyMode } from '@/models/miscelanious'; import { CstMatchMode, DependencyMode } from '@/models/miscelanious';
@ -86,7 +86,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
<SelectorButton transparent tabIndex={-1} <SelectorButton transparent tabIndex={-1}
tooltip='Настройка атрибутов для фильтрации' tooltip='Настройка атрибутов для фильтрации'
dimensions='w-fit h-full' dimensions='w-fit h-full'
icon={<FilterIcon size={5} />} icon={<BiFilterAlt size='1.25rem' />}
text={labelCstMathchMode(filterMatch)} text={labelCstMathchMode(filterMatch)}
onClick={matchModeMenu.toggle} onClick={matchModeMenu.toggle}
/> />
@ -100,7 +100,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
key={`${prefixes.cst_match_mode_list}${index}`} key={`${prefixes.cst_match_mode_list}${index}`}
onClick={() => handleMatchModeChange(matchMode)} onClick={() => handleMatchModeChange(matchMode)}
> >
<p><span className='font-semibold'>{labelCstMathchMode(matchMode)}:</span> {describeCstMathchMode(matchMode)}</p> <p><b>{labelCstMathchMode(matchMode)}:</b> {describeCstMathchMode(matchMode)}</p>
</DropdownButton>); </DropdownButton>);
})} })}
</Dropdown> : null} </Dropdown> : null}
@ -110,7 +110,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
<SelectorButton transparent tabIndex={-1} <SelectorButton transparent tabIndex={-1}
tooltip='Настройка фильтрации по графу термов' tooltip='Настройка фильтрации по графу термов'
dimensions='w-fit h-full pr-2' dimensions='w-fit h-full pr-2'
icon={<CogIcon size={4} />} icon={<BiCog size='1.25rem' />}
text={labelCstSource(filterSource)} text={labelCstSource(filterSource)}
onClick={sourceMenu.toggle} onClick={sourceMenu.toggle}
/> />
@ -124,7 +124,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
key={`${prefixes.cst_source_list}${index}`} key={`${prefixes.cst_source_list}${index}`}
onClick={() => handleSourceChange(source)} onClick={() => handleSourceChange(source)}
> >
<p><span className='font-semibold'>{labelCstSource(source)}:</span> {describeCstSource(source)}</p> <p><b>{labelCstSource(source)}:</b> {describeCstSource(source)}</p>
</DropdownButton>); </DropdownButton>);
})} })}
</Dropdown> : null} </Dropdown> : null}

View File

@ -43,13 +43,6 @@ function ConstituentsTable({
}, [windowSize, denseThreshold]); }, [windowSize, denseThreshold]);
const handleRowClicked = useCallback( const handleRowClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
if (event.altKey && !isMockCst(cst)) {
onOpenEdit(cst.id);
}
}, [onOpenEdit]);
const handleDoubleClick = useCallback(
(cst: IConstituenta) => { (cst: IConstituenta) => {
if (!isMockCst(cst)) { if (!isMockCst(cst)) {
onOpenEdit(cst.id); onOpenEdit(cst.id);
@ -135,7 +128,6 @@ function ConstituentsTable({
</div> </div>
} }
onRowDoubleClicked={handleDoubleClick}
onRowClicked={handleRowClicked} onRowClicked={handleRowClicked}
/>); />);
} }

View File

@ -42,7 +42,7 @@ function ViewConstituents({ expression, baseHeight, schema, activeID, onOpenEdit
activeExpression={expression} activeExpression={expression}
setFiltered={setFilteredData} setFiltered={setFilteredData}
/> />
<div className='overflow-y-auto text-sm overscroll-none' style={{maxHeight : `${maxHeight}`}}> <div className='overflow-y-auto text-sm select-none overscroll-none' style={{maxHeight : `${maxHeight}`}}>
<ConstituentsTable <ConstituentsTable
items={filteredData} items={filteredData}
activeID={activeID} activeID={activeID}

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { BiInfoCircle } from 'react-icons/bi';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Button from '@/components/Common/Button'; import Button from '@/components/Common/Button';
@ -11,7 +12,6 @@ import SubmitButton from '@/components/Common/SubmitButton';
import TextInput from '@/components/Common/TextInput'; import TextInput from '@/components/Common/TextInput';
import TextURL from '@/components/Common/TextURL'; import TextURL from '@/components/Common/TextURL';
import ExpectedAnonymous from '@/components/ExpectedAnonymous'; import ExpectedAnonymous from '@/components/ExpectedAnonymous';
import { HelpIcon } from '@/components/Icons';
import InfoError from '@/components/InfoError'; import InfoError from '@/components/InfoError';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NagivationContext'; import { useConceptNavigation } from '@/context/NagivationContext';
@ -77,7 +77,7 @@ function RegisterPage() {
id={globalIDs.password_tooltip} id={globalIDs.password_tooltip}
position='top-[4.8rem] left-[3.4rem] absolute' position='top-[4.8rem] left-[3.4rem] absolute'
> >
<HelpIcon color='clr-text-primary' size={5} /> <BiInfoCircle size='1.25rem' className='clr-text-primary' />
</Overlay> </Overlay>
<ConceptTooltip <ConceptTooltip
anchorSelect={`#${globalIDs.password_tooltip}`} anchorSelect={`#${globalIDs.password_tooltip}`}

View File

@ -38,8 +38,8 @@ function UserTabs() {
<MiniButton <MiniButton
tooltip='Показать/Скрыть список отслеживаний' tooltip='Показать/Скрыть список отслеживаний'
icon={showSubs icon={showSubs
? <SubscribedIcon color='clr-text-primary' size={5}/> ? <SubscribedIcon size='1.25rem' className='clr-text-primary' />
: <NotSubscribedIcon color='clr-text-primary' size={5}/> : <NotSubscribedIcon size='1.25rem' className='clr-text-primary' />
} }
onClick={() => setShowSubs(prev => !prev)} onClick={() => setShowSubs(prev => !prev)}
/> />

View File

@ -5,7 +5,7 @@
* Description is a long description used in tooltips. * Description is a long description used in tooltips.
*/ */
import { GramData,Grammeme, ReferenceType } from '@/models/language'; import { GramData,Grammeme, ReferenceType } from '@/models/language';
import { CstMatchMode, DependencyMode, HelpTopic, LibraryFilterStrategy } from '@/models/miscelanious'; import { CstMatchMode, DependencyMode, HelpTopic, LibraryFilterStrategy, UserAccessMode } from '@/models/miscelanious';
import { CstClass, CstType, ExpressionStatus, IConstituenta } from '@/models/rsform'; import { CstClass, CstType, ExpressionStatus, IConstituenta } from '@/models/rsform';
import { IArgumentInfo, IRSErrorDescription, ISyntaxTreeNode, ParsingStatus, RSErrorType, TokenID } from '@/models/rslang'; import { IArgumentInfo, IRSErrorDescription, ISyntaxTreeNode, ParsingStatus, RSErrorType, TokenID } from '@/models/rslang';
@ -653,3 +653,28 @@ export function describeRSError(error: IRSErrorDescription): string {
} }
return 'UNKNOWN ERROR'; return 'UNKNOWN ERROR';
} }
/**
* Retrieves label for {@link UserAccessMode}.
*/
export function labelAccessMode(mode: UserAccessMode): string {
switch (mode) {
case UserAccessMode.READER: return 'Читатель';
case UserAccessMode.OWNER: return 'Владелец';
case UserAccessMode.ADMIN: return 'Администратор';
}
}
/**
* Retrieves description for {@link UserAccessMode}.
*/
export function describeAccessMode(mode: UserAccessMode): string {
switch (mode) {
case UserAccessMode.READER:
return 'Режим запрещает редактирование';
case UserAccessMode.OWNER:
return 'Режим редактирования владельцем';
case UserAccessMode.ADMIN:
return 'Режим редактирования администратором';
}
}