mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implement editorial levels
This commit is contained in:
parent
e0dcbd612c
commit
18beffb1d9
|
@ -5,7 +5,10 @@
|
||||||
<link rel="icon" href="/favicon.svg" />
|
<link rel="icon" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Веб-приложение для работы с концептуальными схемами" />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Разработка концептуальных схем. Библиотека концептуальных схем и предметных моделей"
|
||||||
|
/>
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
|
@ -1,105 +1,107 @@
|
||||||
// Search new icons at https://reactsvgicons.com/
|
// Search new icons at https://reactsvgicons.com/
|
||||||
|
// Note: save this file using Ctrl + K, Ctrl + Shift + S to disable autoformat
|
||||||
|
|
||||||
// ==== General actions =======
|
// ==== General actions =======
|
||||||
export { BiMenu as IconMenu } from 'react-icons/bi';
|
export { BiMenu as IconMenu } from 'react-icons/bi';
|
||||||
export { LuLogOut as IconLogout } from 'react-icons/lu';
|
export { LuLogOut as IconLogout } from 'react-icons/lu';
|
||||||
export { FiSave as IconSave } from 'react-icons/fi';
|
export { FiSave as IconSave } from 'react-icons/fi';
|
||||||
export { BiCheck as IconAccept } from 'react-icons/bi';
|
export { BiCheck as IconAccept } from 'react-icons/bi';
|
||||||
export { BiX as IconRemove } from 'react-icons/bi';
|
export { BiX as IconRemove } from 'react-icons/bi';
|
||||||
export { BiTrash as IconDestroy } from 'react-icons/bi';
|
export { BiTrash as IconDestroy } from 'react-icons/bi';
|
||||||
export { BiReset as IconReset } from 'react-icons/bi';
|
export { BiReset as IconReset } from 'react-icons/bi';
|
||||||
export { LiaEdit as IconEdit } from 'react-icons/lia';
|
export { LiaEdit as IconEdit } from 'react-icons/lia';
|
||||||
export { FiEdit as IconEdit2 } from 'react-icons/fi';
|
export { FiEdit as IconEdit2 } from 'react-icons/fi';
|
||||||
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
|
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
|
||||||
export { BiDownload as IconDownload } from 'react-icons/bi';
|
export { BiDownload as IconDownload } from 'react-icons/bi';
|
||||||
export { BiUpload as IconUpload } from 'react-icons/bi';
|
export { BiUpload as IconUpload } from 'react-icons/bi';
|
||||||
export { BiCog as IconSettings } from 'react-icons/bi';
|
export { BiCog as IconSettings } from 'react-icons/bi';
|
||||||
export { BiShareAlt as IconShare } from 'react-icons/bi';
|
export { BiShareAlt as IconShare } from 'react-icons/bi';
|
||||||
export { BiFilterAlt as IconFilter } from 'react-icons/bi';
|
export { BiFilterAlt as IconFilter } from 'react-icons/bi';
|
||||||
export { BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
|
export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
|
||||||
export { LuAlertTriangle as IconAlert } from 'react-icons/lu';
|
export { LuAlertTriangle as IconAlert } from 'react-icons/lu';
|
||||||
|
|
||||||
// ===== UI elements =======
|
// ===== UI elements =======
|
||||||
export { BiX as IconClose } from 'react-icons/bi';
|
export { BiX as IconClose } from 'react-icons/bi';
|
||||||
export { LuChevronDown as IconDropArrow } from 'react-icons/lu';
|
export { LuChevronDown as IconDropArrow } from 'react-icons/lu';
|
||||||
export { LuChevronUp as IconDropArrowUp } from 'react-icons/lu';
|
export { LuChevronUp as IconDropArrowUp } from 'react-icons/lu';
|
||||||
export { RiMenuFoldFill as IconMenuFold } from 'react-icons/ri';
|
export { RiMenuFoldFill as IconMenuFold } from 'react-icons/ri';
|
||||||
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
|
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
|
||||||
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
|
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
|
||||||
export { LuSun as IconLightTheme } from 'react-icons/lu';
|
export { LuSun as IconLightTheme } from 'react-icons/lu';
|
||||||
export { LuLightbulb as IconHelp } from 'react-icons/lu';
|
export { LuLightbulb as IconHelp } from 'react-icons/lu';
|
||||||
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
|
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
|
||||||
export { RiPushpinFill as IconPin } from 'react-icons/ri';
|
export { RiPushpinFill as IconPin } from 'react-icons/ri';
|
||||||
export { RiUnpinLine as IconUnpin } from 'react-icons/ri';
|
export { RiUnpinLine as IconUnpin } from 'react-icons/ri';
|
||||||
export { BiCaretDown as IconSortDesc } from 'react-icons/bi';
|
export { BiCaretDown as IconSortDesc } from 'react-icons/bi';
|
||||||
export { BiCaretUp as IconSortAsc } from 'react-icons/bi';
|
export { BiCaretUp as IconSortAsc } from 'react-icons/bi';
|
||||||
export { BiChevronLeft as IconPageLeft } from 'react-icons/bi';
|
export { BiChevronLeft as IconPageLeft } from 'react-icons/bi';
|
||||||
export { BiChevronRight as IconPageRight } from 'react-icons/bi';
|
export { BiChevronRight as IconPageRight } from 'react-icons/bi';
|
||||||
export { BiFirstPage as IconPageFirst } from 'react-icons/bi';
|
export { BiFirstPage as IconPageFirst } from 'react-icons/bi';
|
||||||
export { BiLastPage as IconPageLast } from 'react-icons/bi';
|
export { BiLastPage as IconPageLast } from 'react-icons/bi';
|
||||||
|
|
||||||
// ==== User status =======
|
// ==== User status =======
|
||||||
export { LuUserCircle2 as IconUser } from 'react-icons/lu';
|
export { LuUserCircle2 as IconUser } from 'react-icons/lu';
|
||||||
export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
|
export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
|
||||||
export { LuCrown as IconOwner } from 'react-icons/lu';
|
export { LuShovel as IconEditor } from 'react-icons/lu';
|
||||||
export { TbMeteor as IconAdmin } from 'react-icons/tb';
|
export { LuCrown as IconOwner } from 'react-icons/lu';
|
||||||
export { TbMeteorOff as IconAdminOff } from 'react-icons/tb';
|
export { TbMeteor as IconAdmin } from 'react-icons/tb';
|
||||||
export { LuGlasses as IconReader } from 'react-icons/lu';
|
export { TbMeteorOff as IconAdminOff } from 'react-icons/tb';
|
||||||
|
export { LuGlasses as IconReader } from 'react-icons/lu';
|
||||||
|
|
||||||
// ===== Domain entities =======
|
// ===== Domain entities =======
|
||||||
export { VscLibrary as IconLibrary } from 'react-icons/vsc';
|
export { VscLibrary as IconLibrary } from 'react-icons/vsc';
|
||||||
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
|
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
|
||||||
export { BiDiamond as IconTemplates } from 'react-icons/bi';
|
export { BiDiamond as IconTemplates } from 'react-icons/bi';
|
||||||
export { LuArchive as IconArchive } from 'react-icons/lu';
|
export { LuArchive as IconArchive } from 'react-icons/lu';
|
||||||
export { LuDatabase as IconDatabase } from 'react-icons/lu';
|
export { LuDatabase as IconDatabase } from 'react-icons/lu';
|
||||||
export { LuImage as IconImage } from 'react-icons/lu';
|
export { LuImage as IconImage } from 'react-icons/lu';
|
||||||
export { TbColumns as IconList } from 'react-icons/tb';
|
export { TbColumns as IconList } from 'react-icons/tb';
|
||||||
export { TbColumnsOff as IconListOff } from 'react-icons/tb';
|
export { TbColumnsOff as IconListOff } from 'react-icons/tb';
|
||||||
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
||||||
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
||||||
export { TbMathFunction as IconFormula } from 'react-icons/tb';
|
export { TbMathFunction as IconFormula } from 'react-icons/tb';
|
||||||
export { BiFontFamily as IconText } from 'react-icons/bi';
|
export { BiFontFamily as IconText } from 'react-icons/bi';
|
||||||
export { BiFont as IconTextOff } from 'react-icons/bi';
|
export { BiFont as IconTextOff } from 'react-icons/bi';
|
||||||
export { RiTreeLine as IconTree } from 'react-icons/ri';
|
export { RiTreeLine as IconTree } from 'react-icons/ri';
|
||||||
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
|
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
|
||||||
export { BiCheckShield as IconImmutable } from 'react-icons/bi';
|
export { BiCheckShield as IconImmutable } from 'react-icons/bi';
|
||||||
export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
|
export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
|
||||||
export { BiBug as IconStatusError } from 'react-icons/bi';
|
export { BiBug as IconStatusError } from 'react-icons/bi';
|
||||||
export { BiCheckCircle as IconStatusOK } from 'react-icons/bi';
|
export { BiCheckCircle as IconStatusOK } from 'react-icons/bi';
|
||||||
export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
|
export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
|
||||||
export { BiPauseCircle as IconStatusIncalculable } from 'react-icons/bi';
|
export { BiPauseCircle as IconStatusIncalculable } from 'react-icons/bi';
|
||||||
export { LuPower as IconKeepAliasOn } from 'react-icons/lu';
|
export { LuPower as IconKeepAliasOn } from 'react-icons/lu';
|
||||||
export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu';
|
export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu';
|
||||||
export { LuFlag as IconKeepTermOn } from 'react-icons/lu';
|
export { LuFlag as IconKeepTermOn } from 'react-icons/lu';
|
||||||
export { LuFlagOff as IconKeepTermOff } from 'react-icons/lu';
|
export { LuFlagOff as IconKeepTermOff } from 'react-icons/lu';
|
||||||
|
|
||||||
// ===== Domain actions =====
|
// ===== Domain actions =====
|
||||||
export { BiUpvote as IconMoveUp } from 'react-icons/bi';
|
export { BiUpvote as IconMoveUp } from 'react-icons/bi';
|
||||||
export { BiDownvote as IconMoveDown } from 'react-icons/bi';
|
export { BiDownvote as IconMoveDown } from 'react-icons/bi';
|
||||||
export { BiRightArrow as IconMoveRight } from 'react-icons/bi';
|
export { BiRightArrow as IconMoveRight } from 'react-icons/bi';
|
||||||
export { BiLeftArrow as IconMoveLeft } from 'react-icons/bi';
|
export { BiLeftArrow as IconMoveLeft } from 'react-icons/bi';
|
||||||
export { FiBell as IconFollow } from 'react-icons/fi';
|
export { FiBell as IconFollow } from 'react-icons/fi';
|
||||||
export { FiBellOff as IconFollowOff } from 'react-icons/fi';
|
export { FiBellOff as IconFollowOff } from 'react-icons/fi';
|
||||||
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
|
export { BiPlusCircle as IconNewItem } from 'react-icons/bi';
|
||||||
export { BiPlusCircle as IconNewItem } from 'react-icons/bi';
|
export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6';
|
||||||
export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6';
|
export { BiDuplicate as IconClone } from 'react-icons/bi';
|
||||||
export { BiDuplicate as IconClone } from 'react-icons/bi';
|
export { LuReplace as IconReplace } from 'react-icons/lu';
|
||||||
export { LuReplace as IconReplace } from 'react-icons/lu';
|
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
|
||||||
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
|
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
|
||||||
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
|
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
|
||||||
export { LuWand2 as IconGenerateNames } from 'react-icons/lu';
|
export { LuWand2 as IconGenerateNames } from 'react-icons/lu';
|
||||||
|
|
||||||
// ======== Graph UI =======
|
// ======== Graph UI =======
|
||||||
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
||||||
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
|
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
|
||||||
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
|
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
|
||||||
export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
|
export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
|
||||||
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
|
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
|
||||||
export { LuAtom as IconGraphCore } from 'react-icons/lu';
|
export { LuAtom as IconGraphCore } from 'react-icons/lu';
|
||||||
export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
|
export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
|
||||||
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
|
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
|
||||||
export { LuSparkles as IconClustering } from 'react-icons/lu';
|
export { LuSparkles as IconClustering } from 'react-icons/lu';
|
||||||
export { LuSparkle as IconClusteringOff } from 'react-icons/lu';
|
export { LuSparkle as IconClusteringOff } from 'react-icons/lu';
|
||||||
|
|
||||||
// ===== Custom elements ======
|
// ===== Custom elements ======
|
||||||
interface IconSVGProps {
|
interface IconSVGProps {
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { useUsers } from '@/context/UsersContext';
|
|
||||||
import { ILibraryItemEx } from '@/models/library';
|
|
||||||
|
|
||||||
import LabeledValue from '../ui/LabeledValue';
|
|
||||||
|
|
||||||
interface InfoLibraryItemProps {
|
|
||||||
item?: ILibraryItemEx;
|
|
||||||
}
|
|
||||||
|
|
||||||
function InfoLibraryItem({ item }: InfoLibraryItemProps) {
|
|
||||||
const { getUserLabel } = useUsers();
|
|
||||||
const intl = useIntl();
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-1'>
|
|
||||||
<LabeledValue label='Владелец' text={getUserLabel(item?.owner ?? null)} />
|
|
||||||
<LabeledValue label='Редакторы' text={item?.editors.length ?? 0} />
|
|
||||||
<LabeledValue label='Отслеживают' text={item?.subscribers.length ?? 0} />
|
|
||||||
<LabeledValue
|
|
||||||
label='Дата обновления'
|
|
||||||
text={item ? new Date(item?.time_update).toLocaleString(intl.locale) : ''}
|
|
||||||
/>
|
|
||||||
<LabeledValue label='Дата создания' text={item ? new Date(item?.time_create).toLocaleString(intl.locale) : ''} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InfoLibraryItem;
|
|
25
rsconcept/frontend/src/components/info/InfoUsers.tsx
Normal file
25
rsconcept/frontend/src/components/info/InfoUsers.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { useUsers } from '@/context/UsersContext';
|
||||||
|
import { UserID } from '@/models/user';
|
||||||
|
|
||||||
|
import { CProps } from '../props';
|
||||||
|
|
||||||
|
interface InfoUsersProps extends CProps.Styling {
|
||||||
|
items: UserID[];
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoUsers({ items, className, prefix, ...restProps }: InfoUsersProps) {
|
||||||
|
const { getUserLabel } = useUsers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx('flex flex-col dense', className)} {...restProps}>
|
||||||
|
{items.map((user, index) => (
|
||||||
|
<div key={`${prefix}${index}`}>{getUserLabel(user)}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InfoUsers;
|
|
@ -15,9 +15,17 @@ interface SelectConstituentaProps extends CProps.Styling {
|
||||||
items?: IConstituenta[];
|
items?: IConstituenta[];
|
||||||
value?: IConstituenta;
|
value?: IConstituenta;
|
||||||
onSelectValue: (newValue?: IConstituenta) => void;
|
onSelectValue: (newValue?: IConstituenta) => void;
|
||||||
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectConstituenta({ className, items, value, onSelectValue, ...restProps }: SelectConstituentaProps) {
|
function SelectConstituenta({
|
||||||
|
className,
|
||||||
|
items,
|
||||||
|
value,
|
||||||
|
onSelectValue,
|
||||||
|
placeholder = 'Выберите конституенту',
|
||||||
|
...restProps
|
||||||
|
}: SelectConstituentaProps) {
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
items?.map(cst => ({
|
items?.map(cst => ({
|
||||||
|
@ -39,10 +47,11 @@ function SelectConstituenta({ className, items, value, onSelectValue, ...restPro
|
||||||
<SelectSingle
|
<SelectSingle
|
||||||
className={clsx('text-ellipsis', className)}
|
className={clsx('text-ellipsis', className)}
|
||||||
options={options}
|
options={options}
|
||||||
value={{ value: value?.id, label: value ? `${value.alias}: ${describeConstituentaTerm(value)}` : '' }}
|
value={value ? { value: value.id, label: `${value.alias}: ${describeConstituentaTerm(value)}` } : undefined}
|
||||||
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
|
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
|
||||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||||
filterOption={filter}
|
filterOption={filter}
|
||||||
|
placeholder={placeholder}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,14 +5,17 @@ import { Grammeme } from '@/models/language';
|
||||||
import { getCompatibleGrams } from '@/models/languageAPI';
|
import { getCompatibleGrams } from '@/models/languageAPI';
|
||||||
import { compareGrammemeOptions, IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
|
import { compareGrammemeOptions, IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
|
||||||
|
|
||||||
interface SelectGrammemeProps extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> {
|
import { CProps } from '../props';
|
||||||
|
|
||||||
|
interface SelectMultiGrammemeProps
|
||||||
|
extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'>,
|
||||||
|
CProps.Styling {
|
||||||
value: IGrammemeOption[];
|
value: IGrammemeOption[];
|
||||||
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
|
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
|
||||||
className?: string;
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectGrammeme({ value, setValue, ...restProps }: SelectGrammemeProps) {
|
function SelectMultiGrammeme({ value, setValue, ...restProps }: SelectMultiGrammemeProps) {
|
||||||
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -32,4 +35,4 @@ function SelectGrammeme({ value, setValue, ...restProps }: SelectGrammemeProps)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectGrammeme;
|
export default SelectMultiGrammeme;
|
62
rsconcept/frontend/src/components/select/SelectUser.tsx
Normal file
62
rsconcept/frontend/src/components/select/SelectUser.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useUsers } from '@/context/UsersContext';
|
||||||
|
import { IUserInfo, UserID } from '@/models/user';
|
||||||
|
import { matchUser } from '@/models/userAPI';
|
||||||
|
|
||||||
|
import { CProps } from '../props';
|
||||||
|
import SelectSingle from '../ui/SelectSingle';
|
||||||
|
|
||||||
|
interface SelectUserProps extends CProps.Styling {
|
||||||
|
items?: IUserInfo[];
|
||||||
|
value?: UserID;
|
||||||
|
placeholder?: string;
|
||||||
|
onSelectValue: (newValue: UserID) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectUser({
|
||||||
|
className,
|
||||||
|
items,
|
||||||
|
value,
|
||||||
|
onSelectValue,
|
||||||
|
placeholder = 'Выберите пользователя',
|
||||||
|
...restProps
|
||||||
|
}: SelectUserProps) {
|
||||||
|
const { getUserLabel } = useUsers();
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return (
|
||||||
|
items?.map(user => ({
|
||||||
|
value: user.id,
|
||||||
|
label: getUserLabel(user.id)
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
}, [items, getUserLabel]);
|
||||||
|
|
||||||
|
const filter = useCallback(
|
||||||
|
(option: { value: UserID | undefined; label: string }, inputValue: string) => {
|
||||||
|
const user = items?.find(item => item.id === option.value);
|
||||||
|
return !user ? false : matchUser(user, inputValue);
|
||||||
|
},
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectSingle
|
||||||
|
className={clsx('text-ellipsis', className)}
|
||||||
|
options={options}
|
||||||
|
value={value ? { value: value, label: getUserLabel(value) } : undefined}
|
||||||
|
onChange={data => {
|
||||||
|
if (data !== null && data.value !== undefined) onSelectValue(data.value);
|
||||||
|
}}
|
||||||
|
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||||
|
filterOption={filter}
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectUser;
|
|
@ -3,7 +3,7 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { IVersionInfo } from '@/models/library';
|
import { IVersionInfo, VersionID } from '@/models/library';
|
||||||
import { labelVersion } from '@/utils/labels';
|
import { labelVersion } from '@/utils/labels';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
@ -12,8 +12,8 @@ import SelectSingle from '../ui/SelectSingle';
|
||||||
interface SelectVersionProps extends CProps.Styling {
|
interface SelectVersionProps extends CProps.Styling {
|
||||||
id?: string;
|
id?: string;
|
||||||
items?: IVersionInfo[];
|
items?: IVersionInfo[];
|
||||||
value?: number;
|
value?: VersionID;
|
||||||
onSelectValue: (newValue?: number) => void;
|
onSelectValue: (newValue?: VersionID) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) {
|
function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) {
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface DividerProps {
|
import { CProps } from '@/components/props';
|
||||||
|
|
||||||
|
interface DividerProps extends CProps.Styling {
|
||||||
vertical?: boolean;
|
vertical?: boolean;
|
||||||
margins?: string;
|
margins?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Divider({ vertical, margins = 'mx-2' }: DividerProps) {
|
function Divider({ vertical, margins = 'mx-2', className, ...restProps }: DividerProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(margins, {
|
className={clsx(
|
||||||
'border-x': vertical,
|
margins, //prettier: split-lines
|
||||||
'border-y': !vertical
|
className,
|
||||||
})}
|
{
|
||||||
|
'border-x': vertical,
|
||||||
|
'border-y': !vertical
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
interface LabeledValueProps {
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { CProps } from '../props';
|
||||||
|
|
||||||
|
interface LabeledValueProps extends CProps.Styling {
|
||||||
id?: string;
|
id?: string;
|
||||||
label: string;
|
label: string;
|
||||||
text: string | number;
|
text: string | number;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LabeledValue({ id, label, text, title }: LabeledValueProps) {
|
function LabeledValue({ id, label, text, title, className, ...restProps }: LabeledValueProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-between gap-3'>
|
<div className={clsx('flex justify-between gap-3', className)} {...restProps}>
|
||||||
<span title={title}>{label}</span>
|
<span title={title}>{label}</span>
|
||||||
<span id={id}>{text}</span>
|
<span id={id}>{text}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
import { createContext, useContext, useState } from 'react';
|
import { createContext, useContext, useState } from 'react';
|
||||||
|
|
||||||
import { UserAccessMode } from '@/models/miscellaneous';
|
import { UserLevel } from '@/models/user';
|
||||||
|
|
||||||
interface IAccessModeContext {
|
interface IAccessModeContext {
|
||||||
mode: UserAccessMode;
|
accessLevel: UserLevel;
|
||||||
setMode: React.Dispatch<React.SetStateAction<UserAccessMode>>;
|
setAccessLevel: React.Dispatch<React.SetStateAction<UserLevel>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccessContext = createContext<IAccessModeContext | null>(null);
|
const AccessContext = createContext<IAccessModeContext | null>(null);
|
||||||
|
@ -23,7 +23,7 @@ interface AccessModeStateProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AccessModeState = ({ children }: AccessModeStateProps) => {
|
export const AccessModeState = ({ children }: AccessModeStateProps) => {
|
||||||
const [mode, setMode] = useState<UserAccessMode>(UserAccessMode.READER);
|
const [accessLevel, setAccessLevel] = useState<UserLevel>(UserLevel.READER);
|
||||||
|
|
||||||
return <AccessContext.Provider value={{ mode, setMode }}>{children}</AccessContext.Provider>;
|
return <AccessContext.Provider value={{ accessLevel, setAccessLevel }}>{children}</AccessContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
getTRSFile,
|
getTRSFile,
|
||||||
patchConstituenta,
|
patchConstituenta,
|
||||||
patchDeleteConstituenta,
|
patchDeleteConstituenta,
|
||||||
|
patchEditorsSet as patchSetEditors,
|
||||||
patchInlineSynthesis,
|
patchInlineSynthesis,
|
||||||
patchLibraryItem,
|
patchLibraryItem,
|
||||||
patchMoveConstituenta,
|
patchMoveConstituenta,
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
patchResetAliases,
|
patchResetAliases,
|
||||||
patchRestoreOrder,
|
patchRestoreOrder,
|
||||||
patchRestoreVersion,
|
patchRestoreVersion,
|
||||||
|
patchSetOwner,
|
||||||
patchSubstituteConstituents,
|
patchSubstituteConstituents,
|
||||||
patchUploadTRS,
|
patchUploadTRS,
|
||||||
patchVersion,
|
patchVersion,
|
||||||
|
@ -26,7 +28,7 @@ import {
|
||||||
} from '@/app/backendAPI';
|
} from '@/app/backendAPI';
|
||||||
import { type ErrorData } from '@/components/info/InfoError';
|
import { type ErrorData } from '@/components/info/InfoError';
|
||||||
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
||||||
import { ILibraryItem, IVersionData } from '@/models/library';
|
import { ILibraryItem, IVersionData, VersionID } from '@/models/library';
|
||||||
import { ILibraryUpdateData } from '@/models/library';
|
import { ILibraryUpdateData } from '@/models/library';
|
||||||
import {
|
import {
|
||||||
ConstituentaID,
|
ConstituentaID,
|
||||||
|
@ -43,6 +45,7 @@ import {
|
||||||
IRSFormUploadData,
|
IRSFormUploadData,
|
||||||
ITargetCst
|
ITargetCst
|
||||||
} from '@/models/rsform';
|
} from '@/models/rsform';
|
||||||
|
import { UserID } from '@/models/user';
|
||||||
|
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
import { useLibrary } from './LibraryContext';
|
import { useLibrary } from './LibraryContext';
|
||||||
|
@ -62,11 +65,14 @@ interface IRSFormContext {
|
||||||
isSubscribed: boolean;
|
isSubscribed: boolean;
|
||||||
|
|
||||||
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
|
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
|
||||||
subscribe: (callback?: () => void) => void;
|
|
||||||
unsubscribe: (callback?: () => void) => void;
|
|
||||||
download: (callback: DataCallback<Blob>) => void;
|
download: (callback: DataCallback<Blob>) => void;
|
||||||
upload: (data: IRSFormUploadData, callback: () => void) => void;
|
upload: (data: IRSFormUploadData, callback: () => void) => void;
|
||||||
|
|
||||||
|
subscribe: (callback?: () => void) => void;
|
||||||
|
unsubscribe: (callback?: () => void) => void;
|
||||||
|
setOwner: (newOwner: UserID, callback?: () => void) => void;
|
||||||
|
setEditors: (newEditors: UserID[], callback?: () => void) => void;
|
||||||
|
|
||||||
resetAliases: (callback: () => void) => void;
|
resetAliases: (callback: () => void) => void;
|
||||||
restoreOrder: (callback: () => void) => void;
|
restoreOrder: (callback: () => void) => void;
|
||||||
produceStructure: (data: ITargetCst, callback?: DataCallback<ConstituentaID[]>) => void;
|
produceStructure: (data: ITargetCst, callback?: DataCallback<ConstituentaID[]>) => void;
|
||||||
|
@ -79,9 +85,9 @@ interface IRSFormContext {
|
||||||
cstDelete: (data: IConstituentaList, callback?: () => void) => void;
|
cstDelete: (data: IConstituentaList, callback?: () => void) => void;
|
||||||
cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void;
|
cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void;
|
||||||
|
|
||||||
versionCreate: (data: IVersionData, callback?: (version: number) => void) => void;
|
versionCreate: (data: IVersionData, callback?: (version: VersionID) => void) => void;
|
||||||
versionUpdate: (target: number, data: IVersionData, callback?: () => void) => void;
|
versionUpdate: (target: VersionID, data: IVersionData, callback?: () => void) => void;
|
||||||
versionDelete: (target: number, callback?: () => void) => void;
|
versionDelete: (target: VersionID, callback?: () => void) => void;
|
||||||
versionRestore: (target: string, callback?: () => void) => void;
|
versionRestore: (target: string, callback?: () => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,6 +234,50 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
[schemaID, setError, schema, user]
|
[schemaID, setError, schema, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setOwner = useCallback(
|
||||||
|
(newOwner: UserID, callback?: () => void) => {
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(undefined);
|
||||||
|
patchSetOwner(schemaID, {
|
||||||
|
data: {
|
||||||
|
user: newOwner
|
||||||
|
},
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: setError,
|
||||||
|
onSuccess: () => {
|
||||||
|
schema.owner = newOwner;
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[schemaID, setError, schema]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setEditors = useCallback(
|
||||||
|
(newEditors: UserID[], callback?: () => void) => {
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(undefined);
|
||||||
|
patchSetEditors(schemaID, {
|
||||||
|
data: {
|
||||||
|
users: newEditors
|
||||||
|
},
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: setError,
|
||||||
|
onSuccess: () => {
|
||||||
|
schema.editors = newEditors;
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[schemaID, setError, schema]
|
||||||
|
);
|
||||||
|
|
||||||
const resetAliases = useCallback(
|
const resetAliases = useCallback(
|
||||||
(callback?: () => void) => {
|
(callback?: () => void) => {
|
||||||
if (!schema || !user) {
|
if (!schema || !user) {
|
||||||
|
@ -522,8 +572,12 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
resetAliases,
|
resetAliases,
|
||||||
produceStructure,
|
produceStructure,
|
||||||
inlineSynthesis,
|
inlineSynthesis,
|
||||||
|
|
||||||
subscribe,
|
subscribe,
|
||||||
unsubscribe,
|
unsubscribe,
|
||||||
|
setOwner,
|
||||||
|
setEditors,
|
||||||
|
|
||||||
cstUpdate,
|
cstUpdate,
|
||||||
cstCreate,
|
cstCreate,
|
||||||
cstRename,
|
cstRename,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { IRSForm } from '@/models/rsform';
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||||
import { labelConstituenta } from '@/utils/labels';
|
import { labelConstituenta } from '@/utils/labels';
|
||||||
|
|
||||||
interface ConstituentsListProps {
|
interface ConstituentsListProps {
|
||||||
list: number[];
|
list: ConstituentaID[];
|
||||||
schema: IRSForm;
|
schema: IRSForm;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
|
@ -5,20 +5,23 @@ import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import Checkbox from '@/components/ui/Checkbox';
|
import Checkbox from '@/components/ui/Checkbox';
|
||||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||||
import { IRSForm } from '@/models/rsform';
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
import ConstituentsList from './ConstituentsList';
|
import ConstituentsList from './ConstituentsList';
|
||||||
|
|
||||||
interface DlgDeleteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
interface DlgDeleteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||||
selected: number[];
|
selected: ConstituentaID[];
|
||||||
onDelete: (items: number[]) => void;
|
onDelete: (items: ConstituentaID[]) => void;
|
||||||
schema: IRSForm;
|
schema: IRSForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) {
|
function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) {
|
||||||
const [expandOut, setExpandOut] = useState(false);
|
const [expandOut, setExpandOut] = useState(false);
|
||||||
const expansion: number[] = useMemo(() => schema.graph.expandAllOutputs(selected), [selected, schema.graph]);
|
const expansion: ConstituentaID[] = useMemo(
|
||||||
|
() => schema.graph.expandAllOutputs(selected), // prettier: split-lines
|
||||||
|
[selected, schema.graph]
|
||||||
|
);
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
hideWindow();
|
hideWindow();
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { IconRemove } from '@/components/Icons';
|
||||||
|
import SelectUser from '@/components/select/SelectUser';
|
||||||
|
import Label from '@/components/ui/Label';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
import Modal from '@/components/ui/Modal';
|
||||||
|
import { useUsers } from '@/context/UsersContext';
|
||||||
|
import { UserID } from '@/models/user';
|
||||||
|
|
||||||
|
import UsersTable from './UsersTable';
|
||||||
|
|
||||||
|
interface DlgEditEditorsProps {
|
||||||
|
editors: UserID[];
|
||||||
|
setEditors: (newValue: UserID[]) => void;
|
||||||
|
hideWindow: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DlgEditEditors({ hideWindow, editors, setEditors }: DlgEditEditorsProps) {
|
||||||
|
const [selected, setSelected] = useState<UserID[]>(editors);
|
||||||
|
const { users } = useUsers();
|
||||||
|
const filtered = useMemo(() => users.filter(user => !selected.includes(user.id)), [users, selected]);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
setEditors(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDeleteEditor = useCallback((target: UserID) => setSelected(prev => prev.filter(id => id !== target)), []);
|
||||||
|
|
||||||
|
const onAddEditor = useCallback((target: UserID) => setSelected(prev => [...prev, target]), []);
|
||||||
|
|
||||||
|
const usersTable = useMemo(
|
||||||
|
() => <UsersTable items={users.filter(user => selected.includes(user.id))} onDelete={onDeleteEditor} />,
|
||||||
|
[users, selected, onDeleteEditor]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
canSubmit
|
||||||
|
header='Список редакторов'
|
||||||
|
submitText='Сохранить список'
|
||||||
|
hideWindow={hideWindow}
|
||||||
|
className='flex flex-col w-[35rem] px-6 gap-3 pb-6'
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<div className={clsx('flex self-center items-center', 'text-sm font-semibold')}>
|
||||||
|
<span>Всего редакторов [{selected.length}]</span>
|
||||||
|
<MiniButton
|
||||||
|
noHover
|
||||||
|
title='Очистить список'
|
||||||
|
className='py-0'
|
||||||
|
icon={<IconRemove size='1.5rem' className='icon-red' />}
|
||||||
|
disabled={selected.length === 0}
|
||||||
|
onClick={() => setSelected([])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{usersTable}
|
||||||
|
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<Label text='Добавить' />
|
||||||
|
<SelectUser items={filtered} value={undefined} onSelectValue={onAddEditor} className='w-[25rem]' />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DlgEditEditors;
|
64
rsconcept/frontend/src/dialogs/DlgEditEditors/UsersTable.tsx
Normal file
64
rsconcept/frontend/src/dialogs/DlgEditEditors/UsersTable.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { IconRemove } from '@/components/Icons';
|
||||||
|
import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
import { IUserInfo, UserID } from '@/models/user';
|
||||||
|
|
||||||
|
interface UsersTableProps {
|
||||||
|
items: IUserInfo[];
|
||||||
|
onDelete: (target: UserID) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<IUserInfo>();
|
||||||
|
|
||||||
|
function UsersTable({ items, onDelete }: UsersTableProps) {
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor('first_name', {
|
||||||
|
id: 'first_name',
|
||||||
|
size: 400,
|
||||||
|
header: 'Имя'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('last_name', {
|
||||||
|
id: 'last_name',
|
||||||
|
size: 400,
|
||||||
|
header: 'Фамилия'
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'actions',
|
||||||
|
size: 50,
|
||||||
|
minSize: 50,
|
||||||
|
maxSize: 50,
|
||||||
|
cell: props => (
|
||||||
|
<div className='h-[1.25rem] w-[1.25rem]'>
|
||||||
|
<MiniButton
|
||||||
|
title='Удалить из списка'
|
||||||
|
noHover
|
||||||
|
noPadding
|
||||||
|
icon={<IconRemove size='1.25rem' className='icon-red' />}
|
||||||
|
onClick={() => onDelete(props.row.original.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
],
|
||||||
|
[onDelete]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
dense
|
||||||
|
noFooter
|
||||||
|
headPosition='0'
|
||||||
|
className='mb-2 border cc-scroll-y'
|
||||||
|
rows={6}
|
||||||
|
data={items}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsersTable;
|
1
rsconcept/frontend/src/dialogs/DlgEditEditors/index.tsx
Normal file
1
rsconcept/frontend/src/dialogs/DlgEditEditors/index.tsx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './DlgEditEditors';
|
|
@ -3,7 +3,7 @@
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
import PickConstituenta from '@/components/select/PickConstituenta';
|
import PickConstituenta from '@/components/select/PickConstituenta';
|
||||||
import SelectGrammeme from '@/components/select/SelectGrammeme';
|
import SelectMultiGrammeme from '@/components/select/SelectMultiGrammeme';
|
||||||
import Label from '@/components/ui/Label';
|
import Label from '@/components/ui/Label';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
|
@ -99,7 +99,7 @@ function EntityTab({ initial, schema, setIsValid, setReference }: EntityTabProps
|
||||||
|
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Label text='Словоформа' />
|
<Label text='Словоформа' />
|
||||||
<SelectGrammeme
|
<SelectMultiGrammeme
|
||||||
id='dlg_reference_grammemes'
|
id='dlg_reference_grammemes'
|
||||||
placeholder='Выберите граммемы'
|
placeholder='Выберите граммемы'
|
||||||
className='flex-grow'
|
className='flex-grow'
|
||||||
|
|
|
@ -8,15 +8,15 @@ import Modal from '@/components/ui/Modal';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
import { IVersionData, IVersionInfo } from '@/models/library';
|
import { IVersionData, IVersionInfo, VersionID } from '@/models/library';
|
||||||
|
|
||||||
import VersionsTable from './VersionsTable';
|
import VersionsTable from './VersionsTable';
|
||||||
|
|
||||||
interface DlgEditVersionsProps {
|
interface DlgEditVersionsProps {
|
||||||
hideWindow: () => void;
|
hideWindow: () => void;
|
||||||
versions: IVersionInfo[];
|
versions: IVersionInfo[];
|
||||||
onDelete: (versionID: number) => void;
|
onDelete: (versionID: VersionID) => void;
|
||||||
onUpdate: (versionID: number, data: IVersionData) => void;
|
onUpdate: (versionID: VersionID, data: IVersionData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVersionsProps) {
|
function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVersionsProps) {
|
||||||
|
|
|
@ -8,14 +8,14 @@ import { IconRemove } from '@/components/Icons';
|
||||||
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
|
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import { useConceptOptions } from '@/context/OptionsContext';
|
import { useConceptOptions } from '@/context/OptionsContext';
|
||||||
import { IVersionInfo } from '@/models/library';
|
import { IVersionInfo, VersionID } from '@/models/library';
|
||||||
|
|
||||||
interface VersionsTableProps {
|
interface VersionsTableProps {
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
items: IVersionInfo[];
|
items: IVersionInfo[];
|
||||||
selected?: number;
|
selected?: VersionID;
|
||||||
onDelete: (versionID: number) => void;
|
onDelete: (versionID: VersionID) => void;
|
||||||
onSelect: (versionID: number) => void;
|
onSelect: (versionID: VersionID) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<IVersionInfo>();
|
const columnHelper = createColumnHelper<IVersionInfo>();
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
import { IconAccept, IconMoveDown, IconMoveLeft, IconMoveRight, IconRemove } from '@/components/Icons';
|
import { IconAccept, IconMoveDown, IconMoveLeft, IconMoveRight, IconRemove } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import SelectGrammeme from '@/components/select/SelectGrammeme';
|
import SelectMultiGrammeme from '@/components/select/SelectMultiGrammeme';
|
||||||
import Label from '@/components/ui/Label';
|
import Label from '@/components/ui/Label';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
|
@ -170,7 +170,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
||||||
onClick={handleInflect}
|
onClick={handleInflect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectGrammeme
|
<SelectMultiGrammeme
|
||||||
placeholder='Выберите граммемы'
|
placeholder='Выберите граммемы'
|
||||||
className='w-[15rem]'
|
className='w-[15rem]'
|
||||||
value={inputGrams}
|
value={inputGrams}
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { DataCallback, postGenerateLexeme, postInflectText, postParseText } from '@/app/backendAPI';
|
||||||
import { ErrorData } from '@/components/info/InfoError';
|
import { ErrorData } from '@/components/info/InfoError';
|
||||||
import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
|
import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
|
||||||
import { DataCallback, postGenerateLexeme, postInflectText, postParseText } from '@/app/backendAPI';
|
|
||||||
|
|
||||||
function useConceptText() {
|
function useConceptText() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
* Module: Models for LibraryItem.
|
* Module: Models for LibraryItem.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { UserID } from './user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents type of library items.
|
* Represents type of library items.
|
||||||
*/
|
*/
|
||||||
|
@ -10,21 +12,26 @@ export enum LibraryItemType {
|
||||||
OPERATIONS_SCHEMA = 'oss'
|
OPERATIONS_SCHEMA = 'oss'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents {@link LibraryItem} identifier type.
|
||||||
|
*/
|
||||||
|
export type LibraryItemID = number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents {@link Version} identifier type.
|
||||||
|
*/
|
||||||
|
export type VersionID = number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents library item version information.
|
* Represents library item version information.
|
||||||
*/
|
*/
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
id: number;
|
id: VersionID;
|
||||||
version: string;
|
version: string;
|
||||||
description: string;
|
description: string;
|
||||||
time_create: string;
|
time_create: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents {@link LibraryItem} identifier type.
|
|
||||||
*/
|
|
||||||
export type LibraryItemID = number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents user data, intended to create or update version metadata in persistent storage.
|
* Represents user data, intended to create or update version metadata in persistent storage.
|
||||||
*/
|
*/
|
||||||
|
@ -43,16 +50,16 @@ export interface ILibraryItem {
|
||||||
is_canonical: boolean;
|
is_canonical: boolean;
|
||||||
time_create: string;
|
time_create: string;
|
||||||
time_update: string;
|
time_update: string;
|
||||||
owner: number | null;
|
owner: UserID | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents library item extended data.
|
* Represents library item extended data.
|
||||||
*/
|
*/
|
||||||
export interface ILibraryItemEx extends ILibraryItem {
|
export interface ILibraryItemEx extends ILibraryItem {
|
||||||
subscribers: number[];
|
subscribers: UserID[];
|
||||||
editors: number[];
|
editors: UserID[];
|
||||||
version?: number;
|
version?: VersionID;
|
||||||
versions: IVersionInfo[];
|
versions: IVersionInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,15 +2,6 @@
|
||||||
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
|
* Module: Miscellaneous frontend model types. Future targets 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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,12 +2,17 @@
|
||||||
* Module: Models for Users.
|
* Module: Models for Users.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents {@link User} identifier type.
|
||||||
|
*/
|
||||||
|
export type UserID = number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents user detailed information.
|
* Represents user detailed information.
|
||||||
* Some information should only be accessible to authorized users
|
* Some information should only be accessible to authorized users
|
||||||
*/
|
*/
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
id: number;
|
id: UserID;
|
||||||
username: string;
|
username: string;
|
||||||
is_staff: boolean;
|
is_staff: boolean;
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -19,7 +24,7 @@ export interface IUser {
|
||||||
* Represents CurrentUser information.
|
* Represents CurrentUser information.
|
||||||
*/
|
*/
|
||||||
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
|
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
|
||||||
subscriptions: number[];
|
subscriptions: UserID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,12 +87,22 @@ export interface IUserUpdatePassword {
|
||||||
* Represents target {@link User}.
|
* Represents target {@link User}.
|
||||||
*/
|
*/
|
||||||
export interface ITargetUser {
|
export interface ITargetUser {
|
||||||
user: number;
|
user: UserID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents target multiple {@link User}.
|
* Represents target multiple {@link User}.
|
||||||
*/
|
*/
|
||||||
export interface ITargetUsers {
|
export interface ITargetUsers {
|
||||||
users: number[];
|
users: UserID[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents user access mode.
|
||||||
|
*/
|
||||||
|
export enum UserLevel {
|
||||||
|
READER = 0,
|
||||||
|
EDITOR,
|
||||||
|
OWNER,
|
||||||
|
ADMIN
|
||||||
}
|
}
|
||||||
|
|
19
rsconcept/frontend/src/models/userAPI.ts
Normal file
19
rsconcept/frontend/src/models/userAPI.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Module: API for formal representation for Users.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TextMatcher } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { IUserInfo } from './user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given target {@link IConstituenta} matches the specified query using the provided matching mode.
|
||||||
|
*
|
||||||
|
* @param target - The target object to be matched.
|
||||||
|
* @param query - The query string used for matching.
|
||||||
|
* @param mode - The matching mode to determine which properties to include in the matching process.
|
||||||
|
*/
|
||||||
|
export function matchUser(target: IUserInfo, query: string): boolean {
|
||||||
|
const matcher = new TextMatcher(query);
|
||||||
|
return matcher.test(target.last_name) || matcher.test(target.first_name);
|
||||||
|
}
|
|
@ -4,41 +4,61 @@ import {
|
||||||
IconClone,
|
IconClone,
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
|
IconEditor,
|
||||||
IconFollow,
|
IconFollow,
|
||||||
IconImmutable,
|
IconImmutable,
|
||||||
IconList,
|
|
||||||
IconNewItem,
|
|
||||||
IconOwner,
|
IconOwner,
|
||||||
IconPublic,
|
IconPublic,
|
||||||
IconSave,
|
IconSave
|
||||||
IconUpload
|
|
||||||
} from '../../../components/Icons';
|
} from '../../../components/Icons';
|
||||||
import LinkTopic from '../../../components/ui/LinkTopic';
|
import LinkTopic from '../../../components/ui/LinkTopic';
|
||||||
|
|
||||||
function HelpRSFormCard() {
|
function HelpRSFormCard() {
|
||||||
// prettier-ignore
|
|
||||||
return (
|
return (
|
||||||
<div className='dense'>
|
<div className='dense'>
|
||||||
<h1>Карточка схемы</h1>
|
<h1>Карточка схемы</h1>
|
||||||
|
|
||||||
<p>Карточка содержит общую информацию и статистику</p>
|
<p>Карточка содержит общую информацию и статистику</p>
|
||||||
<p>Карточка позволяет управлять атрибутами схемы и <LinkTopic text='версиями' topic={HelpTopic.VERSIONS}/></p>
|
<p>
|
||||||
|
Карточка позволяет управлять атрибутами схемы и <LinkTopic text='версиями' topic={HelpTopic.VERSIONS} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Карточка позволяет назначать <IconEditor className='inline-icon' /> Редакторов
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Карточка позволяет изменить <IconOwner className='inline-icon icon-green' /> Владельца
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>Управление</h2>
|
<h2>Управление</h2>
|
||||||
<li><IconSave className='inline-icon'/> сохранить изменения: Ctrl + S</li>
|
<li>
|
||||||
<li><IconOwner className='inline-icon'/> Владелец обладает правом редактирования</li>
|
<IconSave className='inline-icon' /> сохранить изменения: Ctrl + S
|
||||||
<li><IconPublic className='inline-icon'/> Общедоступные схемы доступны для всех</li>
|
</li>
|
||||||
<li><IconImmutable className='inline-icon'/> Неизменные схемы редактируют только администраторы</li>
|
<li>
|
||||||
<li><IconClone className='inline-icon icon-green'/> Клонировать – создать копию схемы</li>
|
<IconEditor className='inline-icon' /> Редактор обладает правом редактирования
|
||||||
<li><IconFollow className='inline-icon'/> Отслеживание – схема в персональном списке</li>
|
</li>
|
||||||
<li><IconDownload className='inline-icon'/> Загрузить/Выгрузить – взаимодействие с Экстеор</li>
|
<li>
|
||||||
<li><IconDestroy className='inline-icon icon-red'/> Удалить – полностью удаляет схему из базы Портала</li>
|
<IconOwner className='inline-icon' /> Владелец обладает полным доступом к схеме
|
||||||
|
</li>
|
||||||
<h2>Версионирование</h2>
|
<li>
|
||||||
<li><IconNewItem className='inline-icon icon-green'/> Создать версию можно только из актуальной схемы</li>
|
<IconPublic className='inline-icon' /> Общедоступные схемы видны всем посетителям
|
||||||
<li><IconUpload className='inline-icon icon-red'/> Загрузить версию в актуальную схему</li>
|
</li>
|
||||||
<li><IconList className='inline-icon'/> Редактировать атрибуты версий</li>
|
<li>
|
||||||
</div>);
|
<IconImmutable className='inline-icon' /> Неизменные схемы редактируют только администраторы
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconFollow className='inline-icon' /> Отслеживание – схема в персональном списке
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconDownload className='inline-icon' /> Загрузить/Выгрузить – взаимодействие с Экстеор
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconDestroy className='inline-icon icon-red' /> Удалить – полностью удаляет схему из базы Портала
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HelpRSFormCard;
|
export default HelpRSFormCard;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconEdit2,
|
IconEdit2,
|
||||||
|
IconEditor,
|
||||||
IconMenu,
|
IconMenu,
|
||||||
IconOwner,
|
IconOwner,
|
||||||
IconReader,
|
IconReader,
|
||||||
|
@ -75,17 +76,22 @@ function HelpRSFormMenu() {
|
||||||
<IconArchive size='1.25rem' className='inline-icon' /> просмотр архивной версии. Переход к актуальной версии
|
<IconArchive size='1.25rem' className='inline-icon' /> просмотр архивной версии. Переход к актуальной версии
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconReader size='1.25rem' className='inline-icon' /> режим "только чтение"
|
<IconReader size='1.25rem' className='inline-icon' /> режим Читатель
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconOwner size='1.25rem' className='inline-icon' /> режим "редактор"
|
<IconEditor size='1.25rem' className='inline-icon' /> режим Редактор
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconAdmin size='1.25rem' className='inline-icon' /> режим "администратор"
|
<IconOwner size='1.25rem' className='inline-icon' /> режим Владелец
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconAdmin size='1.25rem' className='inline-icon' /> режим Администратор
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p>Нижестоящие в списке режимы работы включают все права и доступные функции вышестоящих</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<IconEdit2 size='1.25rem' className='inline-icon icon-green' /> операции над концептуальной схемой описаны в{' '}
|
<IconEdit2 size='1.25rem' className='inline-icon icon-green' /> операции над концептуальной схемой описаны в{' '}
|
||||||
<LinkTopic text='разделе Экспликация' topic={HelpTopic.RSL_OPERATIONS} />.
|
<LinkTopic text='разделе Экспликация' topic={HelpTopic.RSL_OPERATIONS} />.
|
||||||
|
|
|
@ -24,56 +24,90 @@ import {
|
||||||
|
|
||||||
function HelpTermGraph() {
|
function HelpTermGraph() {
|
||||||
const { colors } = useConceptOptions();
|
const { colors } = useConceptOptions();
|
||||||
// prettier-ignore
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<div className='dense w-[14rem]'>
|
<div className='dense w-[14rem]'>
|
||||||
<h1>Настройка графа</h1>
|
<h1>Настройка графа</h1>
|
||||||
<li>Цвет – покраска узлов</li>
|
<li>Цвет – покраска узлов</li>
|
||||||
<li>Граф – расположение</li>
|
<li>Граф – расположение</li>
|
||||||
<li>Размер – размер узлов</li>
|
<li>Размер – размер узлов</li>
|
||||||
<li><IconText className='inline-icon'/> Отображение текста</li>
|
<li>
|
||||||
<li><IconClustering className='inline-icon'/> Скрыть порожденные</li>
|
<IconText className='inline-icon' /> Отображение текста
|
||||||
<li><IconRotate3D className='inline-icon'/> Вращение 3D</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconClustering className='inline-icon' /> Скрыть порожденные
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconRotate3D className='inline-icon' /> Вращение 3D
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider vertical margins='mx-3 mt-3' />
|
||||||
|
|
||||||
|
<div className='dense w-[21rem]'>
|
||||||
|
<h1>Изменение узлов</h1>
|
||||||
|
<li>Клик на конституенту – выделение</li>
|
||||||
|
<li>
|
||||||
|
Ctrl + клик – выбор <span style={{ color: colors.fgPurple }}>фокус-конституенты</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconReset className='inline-icon' /> Esc – сбросить выделение
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconEdit className='inline-icon' /> Двойной клик – редактирование
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconDestroy className='inline-icon' /> Delete – удалить выбранные
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconNewItem className='inline-icon' /> Новая со ссылками на выделенные
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider vertical margins='mx-3 mt-3' />
|
<Divider margins='my-3' />
|
||||||
|
|
||||||
<div className='dense w-[21rem]'>
|
<div className='flex mb-3'>
|
||||||
<h1>Изменение узлов</h1>
|
<div className='dense w-[14rem]'>
|
||||||
<li>Клик на конституенту – выделение</li>
|
<h1>Общие</h1>
|
||||||
<li>Ctrl + клик – выбор <span style={{ color: colors.fgPurple }}>фокус-конституенты</span></li>
|
<li>
|
||||||
<li><IconReset className='inline-icon'/> Esc – сбросить выделение</li>
|
<IconFilter className='inline-icon' /> Открыть настройки
|
||||||
<li><IconEdit className='inline-icon'/> Двойной клик – редактирование</li>
|
</li>
|
||||||
<li><IconDestroy className='inline-icon'/> Delete – удалить выбранные</li>
|
<li>
|
||||||
<li><IconNewItem className='inline-icon'/> Новая со ссылками на выделенные</li>
|
<IconFitImage className='inline-icon' /> Вписать граф в экран
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconImage className='inline-icon' /> Сохранить в формат PNG
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider vertical margins='mx-3' />
|
||||||
|
|
||||||
|
<div className='dense w-[21rem]'>
|
||||||
|
<h1>Выделение</h1>
|
||||||
|
<li>
|
||||||
|
<IconGraphCollapse className='inline-icon' /> все влияющие
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconGraphExpand className='inline-icon' /> все зависимые
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconGraphMaximize className='inline-icon' /> зависимые только от выделенных
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconGraphInputs className='inline-icon' /> входящие напрямую
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconGraphOutputs className='inline-icon' /> исходящие напрямую
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconGraphCore className='inline-icon' /> выделить <LinkTopic text='Ядро' topic={HelpTopic.CC_SYSTEM} />
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<Divider margins='my-3' />
|
|
||||||
|
|
||||||
<div className='flex mb-3'>
|
|
||||||
<div className='dense w-[14rem]'>
|
|
||||||
<h1>Общие</h1>
|
|
||||||
<li><IconFilter className='inline-icon'/> Открыть настройки</li>
|
|
||||||
<li><IconFitImage className='inline-icon'/> Вписать граф в экран</li>
|
|
||||||
<li><IconImage className='inline-icon'/> Сохранить в формат PNG</li>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider vertical margins='mx-3' />
|
|
||||||
|
|
||||||
<div className='dense w-[21rem]'>
|
|
||||||
<h1>Выделение</h1>
|
|
||||||
<li><IconGraphCollapse className='inline-icon'/> все влияющие</li>
|
|
||||||
<li><IconGraphExpand className='inline-icon'/> все зависимые</li>
|
|
||||||
<li><IconGraphMaximize className='inline-icon'/> зависимые только от выделенных</li>
|
|
||||||
<li><IconGraphInputs className='inline-icon'/> входящие напрямую</li>
|
|
||||||
<li><IconGraphOutputs className='inline-icon'/> исходящие напрямую</li>
|
|
||||||
<li><IconGraphCore className='inline-icon'/> выделить <LinkTopic text='Ядро' topic={HelpTopic.CC_SYSTEM} /></li>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HelpTermGraph;
|
export default HelpTermGraph;
|
||||||
|
|
|
@ -1,19 +1,30 @@
|
||||||
import LinkTopic from '@/components/ui/LinkTopic';
|
import { IconEditor, IconList, IconNewItem, IconShare, IconUpload } from '@/components/Icons';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
|
||||||
|
|
||||||
function HelpVersions() {
|
function HelpVersions() {
|
||||||
return (
|
return (
|
||||||
<div className='text-justify'>
|
<div className=''>
|
||||||
<h1>Версионирование схем</h1>
|
<h1>Версионирование схем</h1>
|
||||||
<p>
|
<p>
|
||||||
Версионирование позволяет сохранить текущее состояние схемы под определенным именем (версией) и использовать
|
Версионирование доступно <IconEditor size='1rem' className='inline-icon' /> Редакторам.
|
||||||
ссылку на него для совместной работы. После создания версии ее содержание изменить нельзя
|
|
||||||
</p>
|
</p>
|
||||||
<li>Владелец обладает правом редактирования названий и создания новых версий</li>
|
<p>Версионирование сохраняет текущее состояние схемы под определенным именем (версией) с доступом по ссылке.</p>
|
||||||
|
<p>После создания версии ее содержание изменить нельзя.</p>
|
||||||
|
|
||||||
|
<h2>Действия</h2>
|
||||||
<li>
|
<li>
|
||||||
Управление версиями происходит в <LinkTopic text='Карточке схемы' topic={HelpTopic.UI_RS_CARD} />
|
<IconShare size='1.25rem' className='inline-icon' /> Поделиться включает версию в ссылку
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconUpload size='1.25rem' className='inline-icon icon-red' /> Загрузить версию в актуальную схему
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconNewItem size='1.25rem' className='inline-icon icon-green' /> Создать версию можно только из актуальной
|
||||||
|
схемы
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<IconList size='1.25rem' className='inline-icon' /> Редактировать атрибуты версий
|
||||||
</li>
|
</li>
|
||||||
<li>Функция Поделиться включает версию в ссылку</li>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { IconEdit } from '@/components/Icons';
|
||||||
|
import InfoUsers from '@/components/info/InfoUsers';
|
||||||
|
import SelectUser from '@/components/select/SelectUser';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
import Tooltip from '@/components/ui/Tooltip';
|
||||||
|
import { useAccessMode } from '@/context/AccessModeContext';
|
||||||
|
import { useUsers } from '@/context/UsersContext';
|
||||||
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
|
import { ILibraryItemEx } from '@/models/library';
|
||||||
|
import { UserID, UserLevel } from '@/models/user';
|
||||||
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
|
import LabeledValue from '../../../components/ui/LabeledValue';
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
|
interface EditorLibraryItemProps {
|
||||||
|
item?: ILibraryItemEx;
|
||||||
|
isModified?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditorLibraryItem({ item, isModified }: EditorLibraryItemProps) {
|
||||||
|
const { getUserLabel, users } = useUsers();
|
||||||
|
const controller = useRSEdit();
|
||||||
|
const { accessLevel } = useAccessMode();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const ownerSelector = useDropdown();
|
||||||
|
const onSelectUser = useCallback(
|
||||||
|
(newValue: UserID) => {
|
||||||
|
console.log(newValue);
|
||||||
|
ownerSelector.hide();
|
||||||
|
if (newValue === item?.owner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.setOwner(newValue);
|
||||||
|
},
|
||||||
|
[controller, item?.owner, ownerSelector]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
{accessLevel >= UserLevel.OWNER ? (
|
||||||
|
<Overlay position='top-[-0.5rem] left-[6rem] cc-icons'>
|
||||||
|
<div className='flex items-start'>
|
||||||
|
<MiniButton
|
||||||
|
title='Изменить владельца'
|
||||||
|
noHover
|
||||||
|
onClick={() => ownerSelector.toggle()}
|
||||||
|
icon={<IconEdit size='1rem' className='mt-1 icon-primary' />}
|
||||||
|
disabled={isModified || controller.isProcessing}
|
||||||
|
/>
|
||||||
|
{ownerSelector.isOpen ? (
|
||||||
|
<SelectUser
|
||||||
|
className='w-[20rem] sm:w-[22.5rem] text-sm'
|
||||||
|
items={users}
|
||||||
|
value={item?.owner ?? undefined}
|
||||||
|
onSelectValue={onSelectUser}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
) : null}
|
||||||
|
<LabeledValue className='sm:mb-1' label='Владелец' text={getUserLabel(item?.owner ?? null)} />
|
||||||
|
|
||||||
|
{accessLevel >= UserLevel.OWNER ? (
|
||||||
|
<Overlay position='top-[-0.5rem] left-[6rem] cc-icons'>
|
||||||
|
<div className='flex items-start'>
|
||||||
|
<MiniButton
|
||||||
|
title='Изменить редакторов'
|
||||||
|
noHover
|
||||||
|
onClick={() => controller.promptEditors()}
|
||||||
|
icon={<IconEdit size='1rem' className='mt-1 icon-primary' />}
|
||||||
|
disabled={isModified || controller.isProcessing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
) : null}
|
||||||
|
<LabeledValue id='editor_stats' className='sm:mb-1' label='Редакторы' text={item?.editors.length ?? 0} />
|
||||||
|
<Tooltip anchorSelect='#editor_stats' layer='z-modal-tooltip'>
|
||||||
|
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<LabeledValue id='sub_stats' className='sm:mb-1' label='Отслеживают' text={item?.subscribers.length ?? 0} />
|
||||||
|
<Tooltip anchorSelect='#sub_stats' layer='z-modal-tooltip'>
|
||||||
|
<InfoUsers items={item?.subscribers ?? []} prefix={prefixes.user_subs} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<LabeledValue
|
||||||
|
className='sm:mb-1'
|
||||||
|
label='Дата обновления'
|
||||||
|
text={item ? new Date(item?.time_update).toLocaleString(intl.locale) : ''}
|
||||||
|
/>
|
||||||
|
<LabeledValue label='Дата создания' text={item ? new Date(item?.time_create).toLocaleString(intl.locale) : ''} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditorLibraryItem;
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import InfoLibraryItem from '@/components/info/InfoLibraryItem';
|
|
||||||
import Divider from '@/components/ui/Divider';
|
import Divider from '@/components/ui/Divider';
|
||||||
import FlexColumn from '@/components/ui/FlexColumn';
|
import FlexColumn from '@/components/ui/FlexColumn';
|
||||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
|
@ -10,6 +9,7 @@ import { useAuth } from '@/context/AuthContext';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
import { globals } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
|
|
||||||
|
import EditorLibraryItem from './EditorLibraryItem';
|
||||||
import FormRSForm from './FormRSForm';
|
import FormRSForm from './FormRSForm';
|
||||||
import RSFormStats from './RSFormStats';
|
import RSFormStats from './RSFormStats';
|
||||||
import RSFormToolbar from './RSFormToolbar';
|
import RSFormToolbar from './RSFormToolbar';
|
||||||
|
@ -56,7 +56,7 @@ function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProp
|
||||||
|
|
||||||
<Divider margins='my-1' />
|
<Divider margins='my-1' />
|
||||||
|
|
||||||
<InfoLibraryItem item={schema} />
|
<EditorLibraryItem item={schema} isModified={isModified} />
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
|
|
||||||
<RSFormStats stats={schema?.stats} />
|
<RSFormStats stats={schema?.stats} />
|
||||||
|
|
|
@ -11,7 +11,9 @@ function RSFormStats({ stats }: RSFormStatsProps) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-1 px-4 sm:mt-8 sm:w-[16rem]'>
|
<div className='flex flex-col sm:gap-1 px-4 sm:mt-8 sm:w-[16rem]'>
|
||||||
|
<Divider margins='my-2' className='sm:hidden' />
|
||||||
|
|
||||||
<LabeledValue id='count_all' label='Всего конституент ' text={stats.count_all} />
|
<LabeledValue id='count_all' label='Всего конституент ' text={stats.count_all} />
|
||||||
<LabeledValue id='count_errors' label='Некорректных' text={stats.count_errors} />
|
<LabeledValue id='count_errors' label='Некорректных' text={stats.count_errors} />
|
||||||
{stats.count_property !== 0 ? (
|
{stats.count_property !== 0 ? (
|
||||||
|
|
|
@ -6,7 +6,9 @@ import { IconDestroy, IconDownload, IconFollow, IconFollowOff, IconSave, IconSha
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
import { useAccessMode } from '@/context/AccessModeContext';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
|
import { UserLevel } from '@/models/user';
|
||||||
import { prepareTooltip } from '@/utils/labels';
|
import { prepareTooltip } from '@/utils/labels';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
@ -22,6 +24,7 @@ interface RSFormToolbarProps {
|
||||||
|
|
||||||
function RSFormToolbar({ modified, anonymous, subscribed, onSubmit, onDestroy }: RSFormToolbarProps) {
|
function RSFormToolbar({ modified, anonymous, subscribed, onSubmit, onDestroy }: RSFormToolbarProps) {
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
const { accessLevel } = useAccessMode();
|
||||||
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
|
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='cc-icons'>
|
<Overlay position='top-1 right-1/2 translate-x-1/2' className='cc-icons'>
|
||||||
|
@ -61,7 +64,7 @@ function RSFormToolbar({ modified, anonymous, subscribed, onSubmit, onDestroy }:
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить схему'
|
title='Удалить схему'
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
disabled={!controller.isContentEditable || controller.isProcessing || accessLevel < UserLevel.OWNER}
|
||||||
onClick={onDestroy}
|
onClick={onDestroy}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -21,14 +21,14 @@ import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate';
|
||||||
import DlgCreateCst from '@/dialogs/DlgCreateCst';
|
import DlgCreateCst from '@/dialogs/DlgCreateCst';
|
||||||
import DlgCreateVersion from '@/dialogs/DlgCreateVersion';
|
import DlgCreateVersion from '@/dialogs/DlgCreateVersion';
|
||||||
import DlgDeleteCst from '@/dialogs/DlgDeleteCst';
|
import DlgDeleteCst from '@/dialogs/DlgDeleteCst';
|
||||||
|
import DlgEditEditors from '@/dialogs/DlgEditEditors';
|
||||||
import DlgEditVersions from '@/dialogs/DlgEditVersions';
|
import DlgEditVersions from '@/dialogs/DlgEditVersions';
|
||||||
import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
|
import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
|
||||||
import DlgInlineSynthesis from '@/dialogs/DlgInlineSynthesis';
|
import DlgInlineSynthesis from '@/dialogs/DlgInlineSynthesis';
|
||||||
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
||||||
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
||||||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||||
import { IVersionData } from '@/models/library';
|
import { IVersionData, VersionID } from '@/models/library';
|
||||||
import { UserAccessMode } from '@/models/miscellaneous';
|
|
||||||
import {
|
import {
|
||||||
ConstituentaID,
|
ConstituentaID,
|
||||||
CstType,
|
CstType,
|
||||||
|
@ -45,6 +45,7 @@ import {
|
||||||
TermForm
|
TermForm
|
||||||
} from '@/models/rsform';
|
} from '@/models/rsform';
|
||||||
import { generateAlias } from '@/models/rsformAPI';
|
import { generateAlias } from '@/models/rsformAPI';
|
||||||
|
import { UserID, UserLevel } from '@/models/user';
|
||||||
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
||||||
import { promptUnsaved } from '@/utils/utils';
|
import { promptUnsaved } from '@/utils/utils';
|
||||||
|
|
||||||
|
@ -58,13 +59,17 @@ interface IRSEditContext {
|
||||||
canProduceStructure: boolean;
|
canProduceStructure: boolean;
|
||||||
nothingSelected: boolean;
|
nothingSelected: boolean;
|
||||||
|
|
||||||
|
setOwner: (newOwner: UserID) => void;
|
||||||
|
promptEditors: () => void;
|
||||||
|
toggleSubscribe: () => void;
|
||||||
|
|
||||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||||
select: (target: ConstituentaID) => void;
|
select: (target: ConstituentaID) => void;
|
||||||
deselect: (target: ConstituentaID) => void;
|
deselect: (target: ConstituentaID) => void;
|
||||||
toggleSelect: (target: ConstituentaID) => void;
|
toggleSelect: (target: ConstituentaID) => void;
|
||||||
deselectAll: () => void;
|
deselectAll: () => void;
|
||||||
|
|
||||||
viewVersion: (version?: number, newTab?: boolean) => void;
|
viewVersion: (version?: VersionID, newTab?: boolean) => void;
|
||||||
createVersion: () => void;
|
createVersion: () => void;
|
||||||
restoreVersion: () => void;
|
restoreVersion: () => void;
|
||||||
editVersions: () => void;
|
editVersions: () => void;
|
||||||
|
@ -81,7 +86,6 @@ interface IRSEditContext {
|
||||||
promptClone: () => void;
|
promptClone: () => void;
|
||||||
promptUpload: () => void;
|
promptUpload: () => void;
|
||||||
share: () => void;
|
share: () => void;
|
||||||
toggleSubscribe: () => void;
|
|
||||||
download: () => void;
|
download: () => void;
|
||||||
|
|
||||||
reindex: () => void;
|
reindex: () => void;
|
||||||
|
@ -123,20 +127,17 @@ export const RSEditState = ({
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { adminMode } = useConceptOptions();
|
const { adminMode } = useConceptOptions();
|
||||||
const { mode, setMode } = useAccessMode();
|
const { accessLevel, setAccessLevel } = useAccessMode();
|
||||||
const model = useRSForm();
|
const model = useRSForm();
|
||||||
|
|
||||||
const isMutable = useMemo(() => {
|
const isMutable = useMemo(() => accessLevel > UserLevel.READER, [accessLevel]);
|
||||||
return (
|
|
||||||
mode !== UserAccessMode.READER && ((model.isOwned || (mode === UserAccessMode.ADMIN && user?.is_staff)) ?? false)
|
|
||||||
);
|
|
||||||
}, [user?.is_staff, mode, model.isOwned]);
|
|
||||||
const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]);
|
const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]);
|
||||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||||
|
|
||||||
const [showUpload, setShowUpload] = useState(false);
|
const [showUpload, setShowUpload] = useState(false);
|
||||||
const [showClone, setShowClone] = useState(false);
|
const [showClone, setShowClone] = useState(false);
|
||||||
const [showDeleteCst, setShowDeleteCst] = useState(false);
|
const [showDeleteCst, setShowDeleteCst] = useState(false);
|
||||||
|
const [showEditEditors, setShowEditEditors] = useState(false);
|
||||||
const [showEditTerm, setShowEditTerm] = useState(false);
|
const [showEditTerm, setShowEditTerm] = useState(false);
|
||||||
const [showSubstitute, setShowSubstitute] = useState(false);
|
const [showSubstitute, setShowSubstitute] = useState(false);
|
||||||
const [showCreateVersion, setShowCreateVersion] = useState(false);
|
const [showCreateVersion, setShowCreateVersion] = useState(false);
|
||||||
|
@ -154,20 +155,27 @@ export const RSEditState = ({
|
||||||
|
|
||||||
useLayoutEffect(
|
useLayoutEffect(
|
||||||
() =>
|
() =>
|
||||||
setMode(prev => {
|
setAccessLevel(prev => {
|
||||||
if (user?.is_staff && (prev === UserAccessMode.ADMIN || adminMode)) {
|
if (
|
||||||
return UserAccessMode.ADMIN;
|
prev === UserLevel.EDITOR &&
|
||||||
|
(model.isOwned || user?.is_staff || (user && model.schema?.editors.includes(user.id)))
|
||||||
|
) {
|
||||||
|
return UserLevel.EDITOR;
|
||||||
|
} else if (user?.is_staff && (prev === UserLevel.ADMIN || adminMode)) {
|
||||||
|
return UserLevel.ADMIN;
|
||||||
} else if (model.isOwned) {
|
} else if (model.isOwned) {
|
||||||
return UserAccessMode.OWNER;
|
return UserLevel.OWNER;
|
||||||
|
} else if (user?.id && model.schema?.editors.includes(user?.id)) {
|
||||||
|
return UserLevel.EDITOR;
|
||||||
} else {
|
} else {
|
||||||
return UserAccessMode.READER;
|
return UserLevel.READER;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[model.schema, setMode, model.isOwned, user, adminMode]
|
[model.schema, setAccessLevel, model.isOwned, user, adminMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewVersion = useCallback(
|
const viewVersion = useCallback(
|
||||||
(version?: number, newTab?: boolean) => router.push(urls.schema(model.schemaID, version), newTab),
|
(version?: VersionID, newTab?: boolean) => router.push(urls.schema(model.schemaID, version), newTab),
|
||||||
[router, model]
|
[router, model]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -270,7 +278,7 @@ export const RSEditState = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteVersion = useCallback(
|
const handleDeleteVersion = useCallback(
|
||||||
(versionID: number) => {
|
(versionID: VersionID) => {
|
||||||
if (!model.schema) {
|
if (!model.schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -285,7 +293,7 @@ export const RSEditState = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUpdateVersion = useCallback(
|
const handleUpdateVersion = useCallback(
|
||||||
(versionID: number, data: IVersionData) => {
|
(versionID: VersionID, data: IVersionData) => {
|
||||||
if (!model.schema) {
|
if (!model.schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -473,6 +481,10 @@ export const RSEditState = ({
|
||||||
setShowClone(true);
|
setShowClone(true);
|
||||||
}, [isModified]);
|
}, [isModified]);
|
||||||
|
|
||||||
|
const promptEditors = useCallback(() => {
|
||||||
|
setShowEditEditors(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const download = useCallback(() => {
|
const download = useCallback(() => {
|
||||||
if (isModified && !promptUnsaved()) {
|
if (isModified && !promptUnsaved()) {
|
||||||
return;
|
return;
|
||||||
|
@ -504,6 +516,20 @@ export const RSEditState = ({
|
||||||
}
|
}
|
||||||
}, [model]);
|
}, [model]);
|
||||||
|
|
||||||
|
const setOwner = useCallback(
|
||||||
|
(newOwner: UserID) => {
|
||||||
|
model.setOwner(newOwner, () => toast.success('Владелец обновлен'));
|
||||||
|
},
|
||||||
|
[model]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setEditors = useCallback(
|
||||||
|
(newEditors: UserID[]) => {
|
||||||
|
model.setEditors(newEditors, () => toast.success('Редакторы обновлены'));
|
||||||
|
},
|
||||||
|
[model]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RSEditContext.Provider
|
<RSEditContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -515,6 +541,10 @@ export const RSEditState = ({
|
||||||
canProduceStructure,
|
canProduceStructure,
|
||||||
nothingSelected,
|
nothingSelected,
|
||||||
|
|
||||||
|
toggleSubscribe,
|
||||||
|
setOwner,
|
||||||
|
promptEditors,
|
||||||
|
|
||||||
setSelected: setSelected,
|
setSelected: setSelected,
|
||||||
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
||||||
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
|
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
|
||||||
|
@ -540,7 +570,6 @@ export const RSEditState = ({
|
||||||
promptUpload: () => setShowUpload(true),
|
promptUpload: () => setShowUpload(true),
|
||||||
download,
|
download,
|
||||||
share,
|
share,
|
||||||
toggleSubscribe,
|
|
||||||
|
|
||||||
reindex,
|
reindex,
|
||||||
reorder,
|
reorder,
|
||||||
|
@ -619,6 +648,13 @@ export const RSEditState = ({
|
||||||
onUpdate={handleUpdateVersion}
|
onUpdate={handleUpdateVersion}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showEditEditors ? (
|
||||||
|
<DlgEditEditors
|
||||||
|
hideWindow={() => setShowEditEditors(false)}
|
||||||
|
editors={model.schema.editors}
|
||||||
|
setEditors={setEditors}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{showInlineSynthesis ? (
|
{showInlineSynthesis ? (
|
||||||
<DlgInlineSynthesis
|
<DlgInlineSynthesis
|
||||||
receiver={model.schema}
|
receiver={model.schema}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconEdit2,
|
IconEdit2,
|
||||||
|
IconEditor,
|
||||||
IconGenerateNames,
|
IconGenerateNames,
|
||||||
IconGenerateStructure,
|
IconGenerateStructure,
|
||||||
IconInlineSynthesis,
|
IconInlineSynthesis,
|
||||||
|
@ -31,7 +32,7 @@ import { useAuth } from '@/context/AuthContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { UserAccessMode } from '@/models/miscellaneous';
|
import { UserLevel } from '@/models/user';
|
||||||
import { describeAccessMode, labelAccessMode } from '@/utils/labels';
|
import { describeAccessMode, labelAccessMode } from '@/utils/labels';
|
||||||
|
|
||||||
import { useRSEdit } from './RSEditContext';
|
import { useRSEdit } from './RSEditContext';
|
||||||
|
@ -46,7 +47,7 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const model = useRSForm();
|
const model = useRSForm();
|
||||||
|
|
||||||
const { mode, setMode } = useAccessMode();
|
const { accessLevel, setAccessLevel } = useAccessMode();
|
||||||
|
|
||||||
const schemaMenu = useDropdown();
|
const schemaMenu = useDropdown();
|
||||||
const editMenu = useDropdown();
|
const editMenu = useDropdown();
|
||||||
|
@ -107,9 +108,9 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
||||||
controller.inlineSynthesis();
|
controller.inlineSynthesis();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeMode(newMode: UserAccessMode) {
|
function handleChangeMode(newMode: UserLevel) {
|
||||||
accessMenu.hide();
|
accessMenu.hide();
|
||||||
setMode(newMode);
|
setAccessLevel(newMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateNew() {
|
function handleCreateNew() {
|
||||||
|
@ -165,7 +166,7 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Удалить схему'
|
text='Удалить схему'
|
||||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={controller.isProcessing || accessLevel < UserLevel.OWNER}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -266,14 +267,16 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
||||||
noBorder
|
noBorder
|
||||||
noOutline
|
noOutline
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
title={`Режим ${labelAccessMode(mode)}`}
|
title={`Режим ${labelAccessMode(accessLevel)}`}
|
||||||
hideTitle={accessMenu.isOpen}
|
hideTitle={accessMenu.isOpen}
|
||||||
className='h-full pr-2'
|
className='h-full pr-2'
|
||||||
icon={
|
icon={
|
||||||
mode === UserAccessMode.ADMIN ? (
|
accessLevel === UserLevel.ADMIN ? (
|
||||||
<IconAdmin size='1.25rem' className='icon-primary' />
|
<IconAdmin size='1.25rem' className='icon-primary' />
|
||||||
) : mode === UserAccessMode.OWNER ? (
|
) : accessLevel === UserLevel.OWNER ? (
|
||||||
<IconOwner size='1.25rem' className='icon-primary' />
|
<IconOwner size='1.25rem' className='icon-primary' />
|
||||||
|
) : accessLevel === UserLevel.EDITOR ? (
|
||||||
|
<IconEditor size='1.25rem' className='icon-primary' />
|
||||||
) : (
|
) : (
|
||||||
<IconReader size='1.25rem' className='icon-primary' />
|
<IconReader size='1.25rem' className='icon-primary' />
|
||||||
)
|
)
|
||||||
|
@ -282,24 +285,31 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={accessMenu.isOpen}>
|
<Dropdown isOpen={accessMenu.isOpen}>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={labelAccessMode(UserAccessMode.READER)}
|
text={labelAccessMode(UserLevel.READER)}
|
||||||
title={describeAccessMode(UserAccessMode.READER)}
|
title={describeAccessMode(UserLevel.READER)}
|
||||||
icon={<IconReader size='1rem' className='icon-primary' />}
|
icon={<IconReader size='1rem' className='icon-primary' />}
|
||||||
onClick={() => handleChangeMode(UserAccessMode.READER)}
|
onClick={() => handleChangeMode(UserLevel.READER)}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={labelAccessMode(UserAccessMode.OWNER)}
|
text={labelAccessMode(UserLevel.EDITOR)}
|
||||||
title={describeAccessMode(UserAccessMode.OWNER)}
|
title={describeAccessMode(UserLevel.EDITOR)}
|
||||||
|
icon={<IconEditor size='1rem' className='icon-primary' />}
|
||||||
|
disabled={!model.isOwned && !model.schema?.editors.includes(user.id)}
|
||||||
|
onClick={() => handleChangeMode(UserLevel.EDITOR)}
|
||||||
|
/>
|
||||||
|
<DropdownButton
|
||||||
|
text={labelAccessMode(UserLevel.OWNER)}
|
||||||
|
title={describeAccessMode(UserLevel.OWNER)}
|
||||||
icon={<IconOwner size='1rem' className='icon-primary' />}
|
icon={<IconOwner size='1rem' className='icon-primary' />}
|
||||||
disabled={!model.isOwned}
|
disabled={!model.isOwned}
|
||||||
onClick={() => handleChangeMode(UserAccessMode.OWNER)}
|
onClick={() => handleChangeMode(UserLevel.OWNER)}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={labelAccessMode(UserAccessMode.ADMIN)}
|
text={labelAccessMode(UserLevel.ADMIN)}
|
||||||
title={describeAccessMode(UserAccessMode.ADMIN)}
|
title={describeAccessMode(UserLevel.ADMIN)}
|
||||||
icon={<IconAdmin size='1rem' className='icon-primary' />}
|
icon={<IconAdmin size='1rem' className='icon-primary' />}
|
||||||
disabled={!user?.is_staff}
|
disabled={!user?.is_staff}
|
||||||
onClick={() => handleChangeMode(UserAccessMode.ADMIN)}
|
onClick={() => handleChangeMode(UserLevel.ADMIN)}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -141,6 +141,8 @@ export const prefixes = {
|
||||||
topic_list: 'topic_list_',
|
topic_list: 'topic_list_',
|
||||||
topic_item: 'topic_item_',
|
topic_item: 'topic_item_',
|
||||||
library_list: 'library_list_',
|
library_list: 'library_list_',
|
||||||
|
user_subs: 'user_subs_',
|
||||||
|
user_editors: 'user_editors_',
|
||||||
wordform_list: 'wordform_list_',
|
wordform_list: 'wordform_list_',
|
||||||
rsedit_btn: 'rsedit_btn_',
|
rsedit_btn: 'rsedit_btn_',
|
||||||
dlg_cst_substitutes_list: 'dlg_cst_substitutes_list_'
|
dlg_cst_substitutes_list: 'dlg_cst_substitutes_list_'
|
||||||
|
|
|
@ -12,8 +12,7 @@ import {
|
||||||
GraphColoring,
|
GraphColoring,
|
||||||
GraphSizing,
|
GraphSizing,
|
||||||
HelpTopic,
|
HelpTopic,
|
||||||
LibraryFilterStrategy,
|
LibraryFilterStrategy
|
||||||
UserAccessMode
|
|
||||||
} from '@/models/miscellaneous';
|
} from '@/models/miscellaneous';
|
||||||
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
||||||
import {
|
import {
|
||||||
|
@ -24,6 +23,7 @@ import {
|
||||||
RSErrorType,
|
RSErrorType,
|
||||||
TokenID
|
TokenID
|
||||||
} from '@/models/rslang';
|
} from '@/models/rslang';
|
||||||
|
import { UserLevel } from '@/models/user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates description for {@link IConstituenta}.
|
* Generates description for {@link IConstituenta}.
|
||||||
|
@ -773,29 +773,32 @@ export function describeRSError(error: IRSErrorDescription): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves label for {@link UserAccessMode}.
|
* Retrieves label for {@link UserLevel}.
|
||||||
*/
|
*/
|
||||||
export function labelAccessMode(mode: UserAccessMode): string {
|
export function labelAccessMode(mode: UserLevel): string {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case UserAccessMode.READER: return 'Читатель';
|
case UserLevel.READER: return 'Читатель';
|
||||||
case UserAccessMode.OWNER: return 'Владелец';
|
case UserLevel.EDITOR: return 'Редактор';
|
||||||
case UserAccessMode.ADMIN: return 'Администратор';
|
case UserLevel.OWNER: return 'Владелец';
|
||||||
|
case UserLevel.ADMIN: return 'Администратор';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves description for {@link UserAccessMode}.
|
* Retrieves description for {@link UserLevel}.
|
||||||
*/
|
*/
|
||||||
export function describeAccessMode(mode: UserAccessMode): string {
|
export function describeAccessMode(mode: UserLevel): string {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case UserAccessMode.READER:
|
case UserLevel.READER:
|
||||||
return 'Режим запрещает редактирование';
|
return 'Режим запрещает редактирование';
|
||||||
case UserAccessMode.OWNER:
|
case UserLevel.EDITOR:
|
||||||
return 'Режим редактирования владельцем';
|
return 'Режим редактирования';
|
||||||
case UserAccessMode.ADMIN:
|
case UserLevel.OWNER:
|
||||||
return 'Режим редактирования администратором';
|
return 'Режим владельца';
|
||||||
|
case UserLevel.ADMIN:
|
||||||
|
return 'Режим администратора';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user