Implement editorial levels

This commit is contained in:
IRBorisov 2024-05-27 20:42:34 +03:00
parent e0dcbd612c
commit 18beffb1d9
38 changed files with 864 additions and 324 deletions

View File

@ -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 />

View File

@ -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 {

View File

@ -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;

View 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;

View File

@ -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}
/> />
); );

View File

@ -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;

View 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;

View File

@ -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) {

View File

@ -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}
/> />
); );
} }

View File

@ -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>

View File

@ -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>;
}; };

View File

@ -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,

View File

@ -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;

View File

@ -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();

View File

@ -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;

View 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;

View File

@ -0,0 +1 @@
export { default } from './DlgEditEditors';

View File

@ -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'

View File

@ -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) {

View File

@ -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>();

View File

@ -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}

View File

@ -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);

View File

@ -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[];
} }

View File

@ -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.
*/ */

View File

@ -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
} }

View 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);
}

View File

@ -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;

View File

@ -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} />.

View File

@ -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;

View File

@ -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>
); );
} }

View File

@ -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;

View File

@ -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} />

View File

@ -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 ? (

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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_'

View File

@ -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 'Режим администратора';
} }
} }