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