Rework editor forms

This commit is contained in:
IRBorisov 2023-11-30 02:14:24 +03:00
parent 4b77faf98b
commit 29691e9f6a
30 changed files with 724 additions and 483 deletions

View File

@ -9,13 +9,14 @@ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'child
function SubmitButton({
text = 'ОК', icon, disabled, tooltip, loading,
dimensions = 'w-fit h-fit'
dimensions = 'w-fit h-fit', ...restProps
}: SubmitButtonProps) {
return (
<button type='submit'
title={tooltip}
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-semibold select-none disabled:cursor-not-allowed border rounded clr-btn-primary ${dimensions} ${loading ? ' cursor-progress' : ''}`}
disabled={disabled ?? loading}
{...restProps}
>
{icon ? <span>{icon}</span> : null}
{text ? <span>{text}</span> : null}

View File

@ -1,6 +1,6 @@
import { IConstituenta } from '../../models/rsform';
import ConceptTooltip from '../Common/ConceptTooltip';
import InfoConstituenta from './InfoConstituenta';
import InfoConstituenta from '../Shared/InfoConstituenta';
interface ConstituentaTooltipProps {
data: IConstituenta

View File

@ -1,5 +1,5 @@
import Divider from '../Common/Divider';
import InfoCstStatus from './InfoCstStatus';
import InfoCstStatus from '../Shared/InfoCstStatus';
function HelpConstituenta() {
return (

View File

@ -1,5 +1,5 @@
import Divider from '../Common/Divider';
import InfoCstStatus from './InfoCstStatus';
import InfoCstStatus from '../Shared/InfoCstStatus';
function HelpRSFormItems() {
return (

View File

@ -1,6 +1,6 @@
import Divider from '../Common/Divider';
import InfoCstClass from './InfoCstClass';
import InfoCstStatus from './InfoCstStatus';
import InfoCstClass from '../Shared/InfoCstClass';
import InfoCstStatus from '../Shared/InfoCstStatus';
function HelpTermGraph() {
return (

View File

@ -0,0 +1,38 @@
import { useIntl } from 'react-intl';
import { useUsers } from '../../context/UsersContext';
import { ILibraryItemEx } from '../../models/library';
interface InfoLibraryItemProps {
item?: ILibraryItemEx
}
function InfoLibraryItem({ item }: InfoLibraryItemProps) {
const { getUserLabel } = useUsers();
const intl = useIntl();
return (
<div className='flex flex-col gap-1'>
<div className='flex justify-start'>
<label className='font-semibold'>Владелец:</label>
<span className='min-w-[200px] ml-2 overflow-ellipsis overflow-hidden whitespace-nowrap'>
{getUserLabel(item?.owner ?? null)}
</span>
</div>
<div className='flex justify-start'>
<label className='font-semibold'>Отслеживают:</label>
<span id='subscriber-count' className='ml-2'>
{ item?.subscribers.length ?? 0 }
</span>
</div>
<div className='flex justify-start'>
<label className='font-semibold'>Дата обновления:</label>
<span className='ml-2'>{item && new Date(item?.time_update).toLocaleString(intl.locale)}</span>
</div>
<div className='flex justify-start'>
<label className='font-semibold'>Дата создания:</label>
<span className='ml-8'>{item && new Date(item?.time_create).toLocaleString(intl.locale)}</span>
</div>
</div>);
}
export default InfoLibraryItem;

View File

@ -26,12 +26,12 @@ interface IRSFormContext {
loading: boolean
processing: boolean
editorMode: boolean
adminMode: boolean
isOwned: boolean
isEditable: boolean
isClaimable: boolean
isReadonly: boolean
isTracking: boolean
isForceAdmin: boolean
toggleForceAdmin: () => void
toggleReadonly: () => void
@ -74,7 +74,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID });
const [ processing, setProcessing ] = useState(false);
const [ isForceAdmin, setIsForceAdmin ] = useState(false);
const [ adminMode, setAdminMode ] = useState(false);
const [ isReadonly, setIsReadonly ] = useState(false);
const [ toggleTracking, setToggleTracking ] = useState(false);
@ -88,13 +88,13 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
return (user?.id !== schema?.owner && schema?.is_common && !schema?.is_canonical) ?? false;
}, [user, schema?.owner, schema?.is_common, schema?.is_canonical]);
const isEditable = useMemo(
const editorMode = useMemo(
() => {
return (
!loading && !processing && !isReadonly &&
((isOwned || (isForceAdmin && user?.is_staff)) ?? false)
((isOwned || (adminMode && user?.is_staff)) ?? false)
);
}, [user?.is_staff, isReadonly, isForceAdmin, isOwned, loading, processing]);
}, [user?.is_staff, isReadonly, adminMode, isOwned, loading, processing]);
const isTracking = useMemo(
() => {
@ -322,9 +322,9 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
<RSFormContext.Provider value={{
schema,
error, loading, processing,
isForceAdmin, isReadonly, isOwned, isEditable,
adminMode, isReadonly, isOwned, editorMode,
isClaimable, isTracking,
toggleForceAdmin: () => setIsForceAdmin(prev => !prev),
toggleForceAdmin: () => setAdminMode(prev => !prev),
toggleReadonly: () => setIsReadonly(prev => !prev),
update, download, upload, claim, resetAliases, subscribe, unsubscribe,
cstUpdate, cstCreate, cstRename, cstDelete, cstMoveTo

View File

@ -18,6 +18,7 @@ interface DlgConstituentaTemplateProps
extends Pick<ModalProps, 'hideWindow'> {
schema: IRSForm
onCreate: (data: ICstCreateData) => void
insertAfter?: number
}
export enum TabID {
@ -26,7 +27,7 @@ export enum TabID {
CONSTITUENTA = 2
}
function DlgConstituentaTemplate({ hideWindow, schema, onCreate }: DlgConstituentaTemplateProps) {
function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }: DlgConstituentaTemplateProps) {
const [ validated, setValidated ] = useState(false);
const [ template, updateTemplate ] = usePartialUpdate<ITemplateState>({});
const [ substitutes, updateSubstitutes ] = usePartialUpdate<IArgumentsState>({
@ -35,7 +36,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate }: DlgConstituen
});
const [constituenta, updateConstituenta] = usePartialUpdate<ICstCreateData>({
cst_type: CstType.TERM,
insert_after: null,
insert_after: insertAfter ?? null,
alias: '',
convention: '',
definition_formal: '',

View File

@ -87,11 +87,11 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
return (
<div className='flex flex-col gap-3'>
<div>
<div className='flex justify-between gap-6'>
<div className='flex justify-between'>
<SelectSingle
className='w-full'
options={categorySelector}
placeholder='Выберите категорию'
className='w-full border-none'
options={categorySelector}
value={state.filterCategory && selectedSchema ? {
value: state.filterCategory.id,
label: state.filterCategory.term_raw
@ -100,9 +100,9 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
isClearable
/>
<SelectSingle
placeholder='Выберите источник'
className='min-w-[15rem]'
options={templateSelector}
placeholder='Выберите источник'
value={state.templateID ? { value: state.templateID, label: templates.find(item => item.id == state.templateID)!.title }: null}
onChange={data => partialUpdate({templateID: (data ? data.value : undefined)})}
/>

View File

@ -119,6 +119,13 @@
}
}
::placeholder {
color: var(--cl-fg-60);
.dark & {
color: var(--cd-fg-60);
}
}
[data-color-scheme="dark"] {
color-scheme: dark;
}
@ -329,6 +336,13 @@
@apply border rounded outline-2 outline
}
.cm-editor .cm-placeholder {
color: var(--cl-fg-60);
.dark & {
color: var(--cd-fg-60);
}
}
.rdt_TableCell{
font-size: 0.875rem;
}

View File

@ -84,6 +84,14 @@ export interface ILibraryItem {
owner: number | null
}
/**
* Represents library item extended data.
*/
export interface ILibraryItemEx
extends ILibraryItem {
subscribers: number[]
}
/**
* Represents update data for editing {@link ILibraryItem}.
*/

View File

@ -3,8 +3,7 @@
*/
import { Graph } from '../utils/Graph'
import { ILibraryUpdateData } from './library'
import { ILibraryItem } from './library'
import { ILibraryItemEx, ILibraryUpdateData } from './library'
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang'
/**
@ -163,11 +162,10 @@ export interface IRSFormStats {
* Represents formal explication for set of concepts.
*/
export interface IRSForm
extends ILibraryItem {
extends ILibraryItemEx {
items: IConstituenta[]
stats: IRSFormStats
graph: Graph
subscribers: number[]
}
/**

View File

@ -0,0 +1,76 @@
import { useMemo } from 'react'
import ConceptTooltip from '../../../components/Common/ConceptTooltip'
import MiniButton from '../../../components/Common/MiniButton'
import HelpConstituenta from '../../../components/Help/HelpConstituenta'
import { ArrowsRotateIcon, CloneIcon, DiamondIcon, DumpBinIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../../components/Icons'
interface ConstituentaToolbarProps {
editorMode: boolean
isModified: boolean
onSubmit: () => void
onReset: () => void
onDelete: () => void
onClone: () => void
onCreate: () => void
onTemplates: () => void
}
function ConstituentaToolbar({
editorMode, isModified,
onSubmit, onReset,
onDelete, onClone, onCreate, onTemplates
}: ConstituentaToolbarProps) {
const canSave = useMemo(() => (isModified && editorMode), [isModified, editorMode]);
return (
<div className='relative w-full'>
<div className='absolute right-0 flex items-start justify-center w-full select-none top-1'>
<MiniButton
tooltip='Сохранить изменения'
disabled={!canSave}
icon={<SaveIcon size={5} color={canSave ? 'text-primary' : ''}/>}
onClick={onSubmit}
/>
<MiniButton
tooltip='Сборсить несохраненные изменения'
disabled={!canSave}
onClick={onReset}
icon={<ArrowsRotateIcon size={5} color={canSave ? 'text-primary' : ''} />}
/>
<MiniButton
tooltip='Создать конституенту после данной'
disabled={!editorMode}
onClick={onCreate}
icon={<SmallPlusIcon size={5} color={editorMode ? 'text-success' : ''} />}
/>
<MiniButton
tooltip='Клонировать конституенту'
disabled={!editorMode}
onClick={onClone}
icon={<CloneIcon size={5} color={editorMode ? 'text-success' : ''} />}
/>
<MiniButton
tooltip='Создать конституенту из шаблона'
icon={<DiamondIcon color={editorMode ? 'text-primary': ''} size={5}/>}
disabled={!editorMode}
onClick={onTemplates}
/>
<MiniButton
tooltip='Удалить редактируемую конституенту'
disabled={!editorMode}
onClick={onDelete}
icon={<DumpBinIcon size={5} color={editorMode ? 'text-warning' : ''} />}
/>
<div id='cst-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#cst-help' offset={4}>
<HelpConstituenta />
</ConceptTooltip>
</div>
</div>);
}
export default ConstituentaToolbar;

View File

@ -1,102 +1,45 @@
import { Dispatch, SetStateAction, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import { Dispatch, SetStateAction, useMemo, useState } from 'react';
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
import MiniButton from '../../../components/Common/MiniButton';
import SubmitButton from '../../../components/Common/SubmitButton';
import TextArea from '../../../components/Common/TextArea';
import HelpConstituenta from '../../../components/Help/HelpConstituenta';
import { ArrowsRotateIcon, CloneIcon, DumpBinIcon, EditIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../../components/Icons';
import RefsInput from '../../../components/RefsInput';
import { useRSForm } from '../../../context/RSFormContext';
import useWindowSize from '../../../hooks/useWindowSize';
import { CstType, IConstituenta, ICstCreateData, ICstRenameData, ICstUpdateData } from '../../../models/rsform';
import { CstType, IConstituenta, ICstCreateData, ICstRenameData } from '../../../models/rsform';
import { SyntaxTree } from '../../../models/rslang';
import { labelCstTypification } from '../../../utils/labels';
import EditorRSExpression from './EditorRSExpression';
import { globalIDs } from '../../../utils/constants';
import ConstituentaToolbar from './ConstituentaToolbar';
import FormConstituenta from './FormConstituenta';
import ViewSideConstituents from './ViewSideConstituents';
// Max height of content for left enditor pane
// Max height of content for left enditor pane.
const UNFOLDED_HEIGHT = '59.1rem';
const SIDELIST_HIDE_THRESHOLD = 1100;
// Thershold window width to hide side constituents list.
const SIDELIST_HIDE_THRESHOLD = 1100; // px
interface EditorConstituentaProps {
activeID?: number
activeCst?: IConstituenta | undefined
isModified: boolean
setIsModified: Dispatch<SetStateAction<boolean>>
onOpenEdit: (cstID: number) => void
onShowAST: (expression: string, ast: SyntaxTree) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onRenameCst: (initial: ICstRenameData) => void
onEditTerm: () => void
onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void
isModified: boolean
setIsModified: Dispatch<SetStateAction<boolean>>
onTemplates: (insertAfter?: number) => void
}
function EditorConstituenta({
isModified, setIsModified, activeID, activeCst, onEditTerm,
onShowAST, onCreateCst, onRenameCst, onOpenEdit, onDeleteCst
onShowAST, onCreateCst, onRenameCst, onOpenEdit, onDeleteCst, onTemplates
}: EditorConstituentaProps) {
const windowSize = useWindowSize();
const { schema, processing, isEditable, cstUpdate } = useRSForm();
const { schema, editorMode } = useRSForm();
const [alias, setAlias] = useState('');
const [term, setTerm] = useState('');
const [textDefinition, setTextDefinition] = useState('');
const [expression, setExpression] = useState('');
const [convention, setConvention] = useState('');
const [typification, setTypification] = useState('N/A');
const [toggleReset, setToggleReset] = useState(false);
const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]);
useLayoutEffect(
() => {
if (!activeCst) {
setIsModified(false);
return;
}
setIsModified(
activeCst.term_raw !== term ||
activeCst.definition_raw !== textDefinition ||
activeCst.convention !== convention ||
activeCst.definition_formal !== expression
);
return () => setIsModified(false);
}, [activeCst, activeCst?.term_raw, activeCst?.definition_formal,
activeCst?.definition_raw, activeCst?.convention,
term, textDefinition, expression, convention, setIsModified]);
useLayoutEffect(
() => {
if (activeCst) {
setAlias(activeCst.alias);
setConvention(activeCst.convention || '');
setTerm(activeCst.term_raw || '');
setTextDefinition(activeCst.definition_raw || '');
setExpression(activeCst.definition_formal || '');
setTypification(activeCst ? labelCstTypification(activeCst) : 'N/A');
}
}, [activeCst, onOpenEdit, schema, toggleReset]);
function handleSubmit(event?: React.FormEvent<HTMLFormElement>) {
if (event) {
event.preventDefault();
}
if (!activeID || processing) {
return;
}
const data: ICstUpdateData = {
id: activeID,
alias: alias,
convention: convention,
definition_formal: expression,
definition_raw: textDefinition,
term_raw: term
};
cstUpdate(data, () => toast.success('Изменения сохранены'));
}
const readyForEdit = useMemo(() => (!!activeCst && editorMode), [activeCst, editorMode]);
function handleDelete() {
if (!schema || !activeID) {
@ -139,22 +82,16 @@ function EditorConstituenta({
onCreateCst(data, true);
}
function handleRename() {
if (!activeID || !activeCst) {
return;
function initiateSubmit() {
const element = document.getElementById(globalIDs.constituenta_editor) as HTMLFormElement;
if (element) {
element.requestSubmit();
}
const data: ICstRenameData = {
id: activeID,
alias: activeCst?.alias,
cst_type: activeCst.cst_type
};
onRenameCst(data);
}
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.ctrlKey && event.code === 'KeyS') {
if (isModified) {
handleSubmit();
initiateSubmit();
}
event.preventDefault();
}
@ -162,144 +99,44 @@ function EditorConstituenta({
return (
<div tabIndex={-1}
className='flex max-w-[1500px]'
className='max-w-[1500px]'
onKeyDown={handleInput}
>
<form
onSubmit={handleSubmit}
className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'
>
<div className='relative w-full'>
<div className='absolute top-0 right-0 flex items-start justify-between w-full'>
{activeCst &&
<MiniButton
tooltip={`Редактировать словоформы термина: ${activeCst.term_forms.length}`}
disabled={!isEnabled}
dimensions='w-fit ml-[3.2rem] pt-[0.3rem]'
noHover
onClick={onEditTerm}
icon={<EditIcon size={4} color={isEnabled ? 'text-primary' : ''} />}
/>}
<div className='flex items-center justify-center w-full pl-[4rem]'>
<div className='font-semibold pointer-events-none w-fit'>
<span className='small-caps'>Конституента </span>
<span className='ml-1 small-caps'>{alias}</span>
</div>
<MiniButton noHover
tooltip='Переименовать конституенту'
disabled={!isEnabled}
onClick={handleRename}
icon={<EditIcon size={4} color={isEnabled ? 'text-primary' : ''} />}
/>
</div>
<div className='flex items-center justify-end'>
<MiniButton
tooltip='Сохранить изменения'
disabled={!isModified || !isEnabled}
icon={<SaveIcon size={5} color={isModified && isEnabled ? 'text-primary' : ''}/>}
onClick={() => handleSubmit()}
/>
<MiniButton
tooltip='Сборсить несохраненные изменения'
disabled={!isEnabled || !isModified}
onClick={() => setToggleReset(prev => !prev)}
icon={<ArrowsRotateIcon size={5} color={isEnabled && isModified ? 'text-primary' : ''} />}
/>
<MiniButton
tooltip='Создать конституенту после данной'
disabled={!isEnabled}
onClick={handleCreateCst}
icon={<SmallPlusIcon size={5} color={isEnabled ? 'text-success' : ''} />}
/>
<MiniButton
tooltip='Клонировать конституенту'
disabled={!isEnabled}
onClick={handleCloneCst}
icon={<CloneIcon size={5} color={isEnabled ? 'text-success' : ''} />}
/>
<MiniButton
tooltip='Удалить редактируемую конституенту'
disabled={!isEnabled}
onClick={handleDelete}
icon={<DumpBinIcon size={5} color={isEnabled ? 'text-warning' : ''} />}
/>
<div id='cst-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#cst-help' offset={4}>
<HelpConstituenta />
</ConceptTooltip>
</div>
</div>
</div>
<div className='flex flex-col gap-3 mt-1'>
<RefsInput
label='Термин'
placeholder='Обозначение, используемое в текстовых определениях данной схемы'
items={schema?.items}
value={term}
initialValue={activeCst?.term_raw ?? ''}
resolved={activeCst?.term_resolved ?? ''}
disabled={!isEnabled}
onChange={newValue => setTerm(newValue)}
/>
<TextArea dense noBorder
label='Типизация'
rows={typification.length > 70 ? 2 : 1}
value={typification}
colors='clr-app'
dimensions='w-full'
style={{
resize: 'none'
}}
disabled
/>
<EditorRSExpression
label='Формальное определение'
activeCst={activeCst}
placeholder='Родоструктурное выражение, задающее формальное определение'
value={expression}
disabled={!isEnabled}
<ConstituentaToolbar
editorMode={readyForEdit}
isModified={isModified}
onSubmit={initiateSubmit}
onReset={() => setToggleReset(prev => !prev)}
onDelete={handleDelete}
onClone={handleCloneCst}
onCreate={handleCreateCst}
onTemplates={() => onTemplates(activeID)}
/>
<div className='flex justify-start'>
<div className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'>
<FormConstituenta id={globalIDs.constituenta_editor}
constituenta={activeCst}
isModified={isModified}
toggleReset={toggleReset}
setIsModified={setIsModified}
onShowAST={onShowAST}
onChange={newValue => setExpression(newValue)}
setTypification={setTypification}
onEditTerm={onEditTerm}
onRenameCst={onRenameCst}
/>
<RefsInput
label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения'
items={schema?.items}
value={textDefinition}
initialValue={activeCst?.definition_raw ?? ''}
resolved={activeCst?.definition_resolved ?? ''}
disabled={!isEnabled}
onChange={newValue => setTextDefinition(newValue)}
/>
<TextArea spellCheck
label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение'
value={convention}
disabled={!isEnabled}
onChange={event => setConvention(event.target.value)}
/>
<div className='flex justify-center w-full'>
<SubmitButton
text='Сохранить изменения'
disabled={!isModified || !isEnabled}
icon={<SaveIcon size={6} />}
/>
</div>
</div>
</form>
{(windowSize.width ?? 0) >= SIDELIST_HIDE_THRESHOLD ?
<div className='w-full mt-[2.25rem] border h-fit'>
<ViewSideConstituents
expression={expression}
baseHeight={UNFOLDED_HEIGHT}
activeID={activeID}
onOpenEdit={onOpenEdit}
/>
</div> : null}
{(windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD) ?
<div className='w-full mt-[2.25rem] border h-fit'>
<ViewSideConstituents
expression={activeCst?.definition_formal ?? ''}
baseHeight={UNFOLDED_HEIGHT}
activeID={activeID}
onOpenEdit={onOpenEdit}
/>
</div> : null}
</div>
</div>);
}

View File

@ -0,0 +1,190 @@
import { Dispatch, SetStateAction, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import MiniButton from '../../../components/Common/MiniButton';
import SubmitButton from '../../../components/Common/SubmitButton';
import TextArea from '../../../components/Common/TextArea';
import { EditIcon, SaveIcon } from '../../../components/Icons';
import RefsInput from '../../../components/RefsInput';
import { useRSForm } from '../../../context/RSFormContext';
import { IConstituenta, ICstRenameData, ICstUpdateData } from '../../../models/rsform';
import { SyntaxTree } from '../../../models/rslang';
import { labelCstTypification } from '../../../utils/labels';
import EditorRSExpression from './EditorRSExpression';
interface FormConstituentaProps {
id?: string
constituenta?: IConstituenta
isModified: boolean
toggleReset: boolean
setIsModified: Dispatch<SetStateAction<boolean>>
onRenameCst: (initial: ICstRenameData) => void
onShowAST: (expression: string, ast: SyntaxTree) => void
onEditTerm: () => void
}
function FormConstituenta({
id, isModified, setIsModified,
constituenta, toggleReset,
onRenameCst, onShowAST, onEditTerm
}: FormConstituentaProps) {
const { schema, cstUpdate, editorMode, processing } = useRSForm();
const readyForEdit = useMemo(() => (!!constituenta && editorMode), [constituenta, editorMode]);
const [alias, setAlias] = useState('');
const [term, setTerm] = useState('');
const [textDefinition, setTextDefinition] = useState('');
const [expression, setExpression] = useState('');
const [convention, setConvention] = useState('');
const [typification, setTypification] = useState('N/A');
useLayoutEffect(
() => {
if (!constituenta) {
setIsModified(false);
return;
}
setIsModified(
constituenta.term_raw !== term ||
constituenta.definition_raw !== textDefinition ||
constituenta.convention !== convention ||
constituenta.definition_formal !== expression
);
return () => setIsModified(false);
}, [constituenta, constituenta?.term_raw, constituenta?.definition_formal,
constituenta?.definition_raw, constituenta?.convention,
term, textDefinition, expression, convention, setIsModified]);
useLayoutEffect(
() => {
if (constituenta) {
setAlias(constituenta.alias);
setConvention(constituenta.convention || '');
setTerm(constituenta.term_raw || '');
setTextDefinition(constituenta.definition_raw || '');
setExpression(constituenta.definition_formal || '');
setTypification(constituenta ? labelCstTypification(constituenta) : 'N/A');
}
}, [constituenta, schema, toggleReset]);
function handleSubmit(event?: React.FormEvent<HTMLFormElement>) {
if (event) {
event.preventDefault();
}
if (!constituenta || processing) {
return;
}
const data: ICstUpdateData = {
id: constituenta.id,
alias: alias,
convention: convention,
definition_formal: expression,
definition_raw: textDefinition,
term_raw: term
};
cstUpdate(data, () => toast.success('Изменения сохранены'));
}
function handleRename() {
if (!constituenta) {
return;
}
const data: ICstRenameData = {
id: constituenta.id,
alias: constituenta.alias,
cst_type: constituenta.cst_type
};
onRenameCst(data);
}
return (<>
{readyForEdit ?
<div className='relative'>
<div className='absolute top-0 right-[-3rem] w-full flex justify-start'>
<MiniButton
tooltip={`Редактировать словоформы термина: ${constituenta!.term_forms.length}`}
disabled={!readyForEdit}
noHover
onClick={onEditTerm}
icon={<EditIcon size={4} color={readyForEdit ? 'text-primary' : ''} />}
/>
<div className='pt-1 pl-6 text-sm font-semibold pointer-events-none w-fit'>
<span>Имя </span>
<span className='ml-1'>{constituenta?.alias ?? ''}</span>
</div>
<MiniButton noHover
tooltip='Переименовать конституенту'
disabled={!readyForEdit}
onClick={handleRename}
icon={<EditIcon size={4} color={readyForEdit ? 'text-primary' : ''} />}
/>
</div>
</div> : null}
<form id={id}
className='flex flex-col gap-3 mt-1'
onSubmit={handleSubmit}
>
<RefsInput
label='Термин'
placeholder='Обозначение, используемое в текстовых определениях данной схемы'
items={schema?.items}
value={term}
initialValue={constituenta?.term_raw ?? ''}
resolved={constituenta?.term_resolved ?? ''}
disabled={!readyForEdit}
onChange={newValue => setTerm(newValue)}
/>
<TextArea dense noBorder
label='Типизация'
rows={typification.length > 70 ? 2 : 1}
value={typification}
colors='clr-app'
dimensions='w-full'
style={{
resize: 'none'
}}
disabled
/>
<EditorRSExpression
label='Формальное определение'
activeCst={constituenta}
placeholder='Родоструктурное выражение, задающее формальное определение'
value={expression}
disabled={!readyForEdit}
toggleReset={toggleReset}
onShowAST={onShowAST}
onChange={newValue => setExpression(newValue)}
setTypification={setTypification}
/>
<RefsInput
label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения'
items={schema?.items}
value={textDefinition}
initialValue={constituenta?.definition_raw ?? ''}
resolved={constituenta?.definition_resolved ?? ''}
disabled={!readyForEdit}
onChange={newValue => setTextDefinition(newValue)}
/>
<TextArea spellCheck
label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение'
value={convention}
disabled={!readyForEdit}
onChange={event => setConvention(event.target.value)}
/>
<div className='flex justify-center w-full'>
<SubmitButton
text='Сохранить изменения'
disabled={!isModified || !readyForEdit}
icon={<SaveIcon size={6} />}
/>
</div>
</form>
</>);
}
export default FormConstituenta;

View File

@ -1,22 +1,13 @@
import { Dispatch, SetStateAction, useLayoutEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { toast } from 'react-toastify';
import { Dispatch, SetStateAction } from 'react';
import Checkbox from '../../../components/Common/Checkbox';
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
import Divider from '../../../components/Common/Divider';
import MiniButton from '../../../components/Common/MiniButton';
import SubmitButton from '../../../components/Common/SubmitButton';
import TextArea from '../../../components/Common/TextArea';
import TextInput from '../../../components/Common/TextInput';
import HelpRSFormMeta from '../../../components/Help/HelpRSFormMeta';
import { DownloadIcon, DumpBinIcon, HelpIcon, OwnerIcon, SaveIcon, ShareIcon } from '../../../components/Icons';
import InfoLibraryItem from '../../../components/Shared/InfoLibraryItem';
import { useAuth } from '../../../context/AuthContext';
import { useRSForm } from '../../../context/RSFormContext';
import { useUsers } from '../../../context/UsersContext';
import { LibraryItemType } from '../../../models/library';
import { IRSFormCreateData } from '../../../models/rsform';
import { globalIDs } from '../../../utils/constants';
import FormRSForm from './FormRSForm';
import RSFormStats from './RSFormStats';
import RSFormToolbar from './RSFormToolbar';
interface EditorRSFormProps {
onDestroy: () => void
@ -28,66 +19,20 @@ interface EditorRSFormProps {
}
function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified, onDownload }: EditorRSFormProps) {
const intl = useIntl();
const { getUserLabel } = useUsers();
const {
schema, update, isForceAdmin,
isEditable, isClaimable, processing
} = useRSForm();
const { schema, editorMode: isEditable, isClaimable } = useRSForm();
const { user } = useAuth();
const [title, setTitle] = useState('');
const [alias, setAlias] = useState('');
const [comment, setComment] = useState('');
const [common, setCommon] = useState(false);
const [canonical, setCanonical] = useState(false);
useLayoutEffect(() => {
if (!schema) {
setIsModified(false);
return;
function initiateSubmit() {
const element = document.getElementById(globalIDs.library_item_editor) as HTMLFormElement;
if (element) {
element.requestSubmit();
}
setIsModified(
schema.title !== title ||
schema.alias !== alias ||
schema.comment !== comment ||
schema.is_common !== common ||
schema.is_canonical !== canonical
);
return () => setIsModified(false);
}, [schema, schema?.title, schema?.alias, schema?.comment,
schema?.is_common, schema?.is_canonical,
title, alias, comment, common, canonical, setIsModified]);
useLayoutEffect(() => {
if (schema) {
setTitle(schema.title);
setAlias(schema.alias);
setComment(schema.comment);
setCommon(schema.is_common);
setCanonical(schema.is_canonical);
}
}, [schema]);
const handleSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
if (event) {
event.preventDefault();
}
const data: IRSFormCreateData = {
item_type: LibraryItemType.RSFORM,
title: title,
alias: alias,
comment: comment,
is_common: common,
is_canonical: canonical
};
update(data, () => toast.success('Изменения сохранены'));
};
}
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.ctrlKey && event.code === 'KeyS') {
if (isModified) {
handleSubmit();
initiateSubmit();
}
event.preventDefault();
}
@ -95,116 +40,31 @@ function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified,
return (
<div tabIndex={-1} onKeyDown={handleInput}>
<div className='relative flex items-start justify-center w-full'>
<div className='absolute flex mt-1'>
<MiniButton
tooltip='Сохранить изменения'
disabled={!isModified || !isEditable}
icon={<SaveIcon size={5} color={isModified && isEditable ? 'text-primary' : ''}/>}
onClick={() => handleSubmit()}
/>
<MiniButton
tooltip='Поделиться схемой'
icon={<ShareIcon size={5} color='text-primary'/>}
onClick={onShare}
/>
<MiniButton
tooltip='Скачать TRS файл'
icon={<DownloadIcon size={5} color='text-primary'/>}
onClick={onDownload}
/>
<MiniButton
tooltip={isClaimable ? 'Стать владельцем' : 'Невозможно стать владельцем' }
icon={<OwnerIcon size={5} color={!isClaimable ? '' : 'text-success'}/>}
disabled={!isClaimable || !user}
onClick={onClaim}
/>
<MiniButton
tooltip='Удалить схему'
disabled={!isEditable}
onClick={onDestroy}
icon={<DumpBinIcon size={5} color={isEditable ? 'text-warning' : ''} />}
/>
<div id='rsform-help' className='py-1 ml-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#rsform-help'>
<HelpRSFormMeta />
</ConceptTooltip>
</div>
</div>
<div className='flex w-full'>
<form onSubmit={handleSubmit} className='flex-grow max-w-[40rem] min-w-[30rem] px-4 py-2'>
<div className='flex flex-col gap-3 mt-2'>
<TextInput id='title' label='Полное название' type='text'
required
value={title}
disabled={!isEditable}
onChange={event => setTitle(event.target.value)}
/>
<TextInput id='alias' label='Сокращение' type='text'
required
value={alias}
disabled={!isEditable}
dense
dimensions='w-full'
onChange={event => setAlias(event.target.value)}
/>
<TextArea id='comment' label='Комментарий'
value={comment}
disabled={!isEditable}
onChange={event => setComment(event.target.value)}
/>
<div className='flex justify-between whitespace-nowrap'>
<Checkbox id='common' label='Общедоступная схема'
tooltip='Общедоступные схемы видны всем пользователям и могут быть изменены'
value={common}
dimensions='w-fit'
disabled={!isEditable}
setValue={value => setCommon(value)}
/>
<Checkbox id='canonical' label='Неизменная схема'
dimensions='w-fit'
value={canonical}
tooltip='Только администраторы могут присваивать схемам неизменный статус'
disabled={!isEditable || !isForceAdmin}
setValue={value => setCanonical(value)}
/>
</div>
<div className='flex justify-center w-full'>
<SubmitButton
text='Сохранить изменения'
loading={processing}
disabled={!isModified || !isEditable}
icon={<SaveIcon size={6} />}
dimensions='my-2 w-fit'
/>
</div>
<RSFormToolbar
editorMode={isEditable}
modified={isModified}
claimable={isClaimable}
anonymous={!user}
<div className='flex flex-col gap-1'>
<div className='flex justify-start'>
<label className='font-semibold'>Владелец:</label>
<span className='min-w-[200px] ml-2 overflow-ellipsis overflow-hidden whitespace-nowrap'>
{getUserLabel(schema?.owner ?? null)}
</span>
</div>
<div className='flex justify-start'>
<label className='font-semibold'>Отслеживают:</label>
<span id='subscriber-count' className='ml-2'>
{ schema?.subscribers.length ?? 0 }
</span>
</div>
<div className='flex justify-start'>
<label className='font-semibold'>Дата обновления:</label>
<span className='ml-2'>{schema && new Date(schema?.time_update).toLocaleString(intl.locale)}</span>
</div>
<div className='flex justify-start'>
<label className='font-semibold'>Дата создания:</label>
<span className='ml-8'>{schema && new Date(schema?.time_create).toLocaleString(intl.locale)}</span>
</div>
</div>
onSubmit={initiateSubmit}
onShare={onShare}
onDownload={onDownload}
onClaim={onClaim}
onDestroy={onDestroy}
/>
<div className='flex w-full'>
<div className='flex-grow max-w-[40rem] min-w-[30rem] px-4 py-2'>
<div className='flex flex-col gap-3 mt-2'>
<FormRSForm id={globalIDs.library_item_editor}
isModified={isModified}
setIsModified={setIsModified}
/>
<Divider margins='my-2' />
<InfoLibraryItem item={schema} />
</div>
</form>
</div>
<Divider vertical />

View File

@ -0,0 +1,131 @@
import { Dispatch, SetStateAction, useLayoutEffect, useState } from 'react';
import { toast } from 'react-toastify';
import Checkbox from '../../../components/Common/Checkbox';
import SubmitButton from '../../../components/Common/SubmitButton';
import TextArea from '../../../components/Common/TextArea';
import TextInput from '../../../components/Common/TextInput';
import { SaveIcon } from '../../../components/Icons';
import { useRSForm } from '../../../context/RSFormContext';
import { LibraryItemType } from '../../../models/library';
import { IRSFormCreateData } from '../../../models/rsform';
interface FormRSFormProps {
id?: string
isModified: boolean
setIsModified: Dispatch<SetStateAction<boolean>>
}
function FormRSForm({
id, isModified, setIsModified,
}: FormRSFormProps) {
const {
schema, update, adminMode: adminMode,
editorMode: editorMode, processing
} = useRSForm();
const [title, setTitle] = useState('');
const [alias, setAlias] = useState('');
const [comment, setComment] = useState('');
const [common, setCommon] = useState(false);
const [canonical, setCanonical] = useState(false);
useLayoutEffect(
() => {
if (!schema) {
setIsModified(false);
return;
}
setIsModified(
schema.title !== title ||
schema.alias !== alias ||
schema.comment !== comment ||
schema.is_common !== common ||
schema.is_canonical !== canonical
);
return () => setIsModified(false);
}, [schema, schema?.title, schema?.alias, schema?.comment,
schema?.is_common, schema?.is_canonical,
title, alias, comment, common, canonical, setIsModified]);
useLayoutEffect(
() => {
if (schema) {
setTitle(schema.title);
setAlias(schema.alias);
setComment(schema.comment);
setCommon(schema.is_common);
setCanonical(schema.is_canonical);
}
}, [schema]);
const handleSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
if (event) {
event.preventDefault();
}
const data: IRSFormCreateData = {
item_type: LibraryItemType.RSFORM,
title: title,
alias: alias,
comment: comment,
is_common: common,
is_canonical: canonical
};
update(data, () => toast.success('Изменения сохранены'));
};
return (
<form id={id}
className='flex flex-col gap-3 mt-2'
onSubmit={handleSubmit}
>
<TextInput required
label='Полное название'
value={title}
disabled={!editorMode}
onChange={event => setTitle(event.target.value)}
/>
<TextInput required dense
label='Сокращение'
dimensions='w-full'
disabled={!editorMode}
value={alias}
onChange={event => setAlias(event.target.value)}
/>
<TextArea
label='Комментарий'
value={comment}
disabled={!editorMode}
onChange={event => setComment(event.target.value)}
/>
<div className='flex justify-between whitespace-nowrap'>
<Checkbox
label='Общедоступная схема'
tooltip='Общедоступные схемы видны всем пользователям и могут быть изменены'
dimensions='w-fit'
disabled={!editorMode}
value={common}
setValue={value => setCommon(value)}
/>
<Checkbox
label='Неизменная схема'
tooltip='Только администраторы могут присваивать схемам неизменный статус'
dimensions='w-fit'
disabled={!editorMode || !adminMode}
value={canonical}
setValue={value => setCanonical(value)}
/>
</div>
<div className='flex justify-center w-full'>
<SubmitButton
text='Сохранить изменения'
loading={processing}
disabled={!isModified || !editorMode}
icon={<SaveIcon size={6} />}
dimensions='my-2 w-fit'
/>
</div>
</form>);
}
export default FormRSForm;

View File

@ -0,0 +1,68 @@
import { useMemo } from 'react'
import ConceptTooltip from '../../../components/Common/ConceptTooltip'
import MiniButton from '../../../components/Common/MiniButton'
import HelpRSFormMeta from '../../../components/Help/HelpRSFormMeta'
import { DownloadIcon, DumpBinIcon, HelpIcon, OwnerIcon, SaveIcon, ShareIcon } from '../../../components/Icons'
interface RSFormToolbarProps {
editorMode: boolean
modified: boolean
claimable: boolean
anonymous: boolean
onSubmit: () => void
onShare: () => void
onDownload: () => void
onClaim: () => void
onDestroy: () => void
}
function RSFormToolbar({
editorMode, modified, claimable, anonymous,
onSubmit, onShare, onDownload,
onClaim, onDestroy
}: RSFormToolbarProps) {
const canSave = useMemo(() => (modified && editorMode), [modified, editorMode]);
return (
<div className='relative flex items-start justify-center w-full'>
<div className='absolute flex mt-1'>
<MiniButton
tooltip='Сохранить изменения'
disabled={!canSave}
icon={<SaveIcon size={5} color={canSave ? 'text-primary' : ''}/>}
onClick={onSubmit}
/>
<MiniButton
tooltip='Поделиться схемой'
icon={<ShareIcon size={5} color='text-primary'/>}
onClick={onShare}
/>
<MiniButton
tooltip='Скачать TRS файл'
icon={<DownloadIcon size={5} color='text-primary'/>}
onClick={onDownload}
/>
<MiniButton
tooltip={claimable ? 'Стать владельцем' : 'Невозможно стать владельцем' }
icon={<OwnerIcon size={5} color={!claimable ? '' : 'text-success'}/>}
disabled={!claimable || anonymous}
onClick={onClaim}
/>
<MiniButton
tooltip='Удалить схему'
disabled={!editorMode}
onClick={onDestroy}
icon={<DumpBinIcon size={5} color={editorMode ? 'text-warning' : ''} />}
/>
<div id='rsform-help' className='py-1 ml-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#rsform-help'>
<HelpRSFormMeta />
</ConceptTooltip>
</div>
</div>);
}
export default RSFormToolbar;

View File

@ -20,7 +20,7 @@ const columnHelper = createColumnHelper<IConstituenta>();
interface EditorRSListProps {
onOpenEdit: (cstID: number) => void
onTemplates: (selected: number[]) => void
onTemplates: (insertAfter?: number) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
}
@ -28,7 +28,7 @@ interface EditorRSListProps {
function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: EditorRSListProps) {
const { colors, mainHeight } = useConceptTheme();
const windowSize = useWindowSize();
const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm();
const { schema, editorMode: isEditable, cstMoveTo, resetAliases } = useRSForm();
const [selected, setSelected] = useState<number[]>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
@ -307,7 +307,7 @@ function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edi
onClone={handleClone}
onCreate={handleCreateCst}
onDelete={handleDelete}
onTemplates={() => onTemplates(selected)}
onTemplates={() => onTemplates(selected.length !== 0 ? selected[selected.length-1] : undefined)}
onReindex={handleReindex}
/>
</div>

View File

@ -88,14 +88,12 @@ function RSListToolbar({
})}
</Dropdown>}
</div>
<MiniButton
tooltip='Создать конституенту из шаблона'
icon={<DiamondIcon color={editorMode ? 'text-primary': ''} size={5}/>}
disabled={!editorMode}
onClick={onTemplates}
/>
<MiniButton
tooltip='Сброс имен: присвоить порядковые имена'
icon={<UpdateIcon color={editorMode ? 'text-primary': ''} size={5}/>}

View File

@ -1,7 +1,7 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { GraphEdge, GraphNode, LayoutTypes } from 'reagraph';
import InfoConstituenta from '../../../components/Help/InfoConstituenta';
import InfoConstituenta from '../../../components/Shared/InfoConstituenta';
import { useRSForm } from '../../../context/RSFormContext';
import { useConceptTheme } from '../../../context/ThemeContext';
import DlgGraphParams from '../../../dialogs/DlgGraphParams';
@ -12,6 +12,7 @@ import { colorbgGraphNode } from '../../../utils/color';
import { TIMEOUT_GRAPH_REFRESH } from '../../../utils/constants';
import GraphSidebar from './GraphSidebar';
import GraphToolbar from './GraphToolbar';
import SelectedCounter from './SelectedCounter';
import TermGraph from './TermGraph';
import useGraphFilter from './useGraphFilter';
import ViewHidden from './ViewHidden';
@ -23,7 +24,7 @@ interface EditorTermGraphProps {
}
function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) {
const { schema, isEditable } = useRSForm();
const { schema, editorMode: isEditable } = useRSForm();
const { colors } = useConceptTheme();
const [toggleDataUpdate, setToggleDataUpdate] = useState(false);
@ -196,12 +197,10 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
onConfirm={handleChangeParams}
/> : null}
{selected.length > 0 ?
<div className='relative w-full z-pop'>
<div className='absolute top-0 left-0 px-2 select-none whitespace-nowrap small-caps clr-app'>
Выбор {selected.length} из {schema?.stats?.count_all ?? 0}
</div>
</div> : null}
<SelectedCounter
total={schema?.stats?.count_all ?? 0}
selected={selected.length}
/>
<GraphToolbar
editorMode={isEditable}
@ -233,7 +232,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
</div> : null}
<div className='relative z-pop'>
<div className='absolute top-0 left-0 flex flex-col max-w-[13.5rem] min-w-[13.5rem]'>
<div className='absolute top-0 left-0 flex flex-col gap-3 max-w-[13.5rem] min-w-[13.5rem]'>
<GraphSidebar
coloring={coloringScheme}
layout={layout}

View File

@ -18,19 +18,17 @@ function GraphSidebar({
layout, setLayout
} : GraphSidebarProps) {
return (
<div className='flex flex-col px-2 pb-2 mt-8 text-sm select-none h-fit'>
<div className='flex items-center w-full gap-1 text-sm'>
<SelectSingle
placeholder='Выберите цвет'
options={SelectorGraphColoring}
isSearchable={false}
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
/>
</div>
<div className='flex flex-col px-2 pb-2 text-sm select-none mt-9 h-fit'>
<SelectSingle
placeholder='Выберите цвет'
options={SelectorGraphColoring}
isSearchable={false}
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
/>
<SelectSingle
placeholder='Способ расположения'
className='w-full mt-1'
className='w-full'
options={SelectorGraphLayout}
isSearchable={false}
value={layout ? { value: layout, label: mapLableLayout.get(layout) } : null}

View File

@ -29,47 +29,47 @@ function GraphToolbar({
} : GraphToolbarProps) {
return (
<div className='relative w-full z-pop'>
<div className='absolute right-0 flex items-start justify-center w-full top-1'>
<MiniButton
tooltip='Настройки фильтрации узлов и связей'
icon={<FilterIcon color='text-primary' size={5} />}
onClick={showParamsDialog} />
<MiniButton
tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={
!noText
? <LetterALinesIcon color='text-success' size={5} />
: <LetterAIcon color='text-primary' size={5} />
}
onClick={toggleNoText} />
<MiniButton
tooltip='Новая конституента'
icon={<SmallPlusIcon color={editorMode ? 'text-success' : ''} size={5} />}
disabled={!editorMode}
onClick={onCreate} />
<MiniButton
tooltip='Удалить выбранные'
icon={<DumpBinIcon color={editorMode && !nothingSelected ? 'text-warning' : ''} size={5} />}
disabled={!editorMode || nothingSelected}
onClick={onDelete} />
<MiniButton
icon={<ArrowsFocusIcon color='text-primary' size={5} />}
tooltip='Восстановить камеру'
onClick={onResetViewpoint} />
<MiniButton
icon={<PlanetIcon color={!is3D ? '' : orbit ? 'text-success' : 'text-primary'} size={5} />}
tooltip='Анимация вращения'
disabled={!is3D}
onClick={toggleOrbit} />
<div className='px-1 py-1' id='items-graph-help'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#items-graph-help'>
<div className='text-sm max-w-[calc(100vw-20rem)] z-tooltip'>
<HelpTermGraph />
</div>
</ConceptTooltip>
<div className='absolute right-0 flex items-start justify-center w-full top-1'>
<MiniButton
tooltip='Настройки фильтрации узлов и связей'
icon={<FilterIcon color='text-primary' size={5} />}
onClick={showParamsDialog} />
<MiniButton
tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={
!noText
? <LetterALinesIcon color='text-success' size={5} />
: <LetterAIcon color='text-primary' size={5} />
}
onClick={toggleNoText} />
<MiniButton
tooltip='Новая конституента'
icon={<SmallPlusIcon color={editorMode ? 'text-success' : ''} size={5} />}
disabled={!editorMode}
onClick={onCreate} />
<MiniButton
tooltip='Удалить выбранные'
icon={<DumpBinIcon color={editorMode && !nothingSelected ? 'text-warning' : ''} size={5} />}
disabled={!editorMode || nothingSelected}
onClick={onDelete} />
<MiniButton
icon={<ArrowsFocusIcon color='text-primary' size={5} />}
tooltip='Восстановить камеру'
onClick={onResetViewpoint} />
<MiniButton
icon={<PlanetIcon color={!is3D ? '' : orbit ? 'text-success' : 'text-primary'} size={5} />}
tooltip='Анимация вращения'
disabled={!is3D}
onClick={toggleOrbit} />
<div className='px-1 py-1' id='items-graph-help'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#items-graph-help'>
<div className='text-sm max-w-[calc(100vw-20rem)] z-tooltip'>
<HelpTermGraph />
</div>
</ConceptTooltip>
</div>
</div>);
}

View File

@ -0,0 +1,18 @@
interface SelectedCounterProps {
total: number
selected: number
}
function SelectedCounter({ total, selected } : SelectedCounterProps) {
if (selected === 0) {
return null;
}
return (
<div className='relative w-full z-pop'>
<div className='absolute left-0 px-2 select-none top-[0.3rem] whitespace-nowrap small-caps clr-app'>
Выбор {selected} из {total}
</div>
</div>);
}
export default SelectedCounter;

View File

@ -89,6 +89,7 @@ function RSTabs() {
const [showEditTerm, setShowEditTerm] = useState(false);
const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined);
const [showTemplates, setShowTemplates] = useState(false);
const panelHeight = useMemo(
@ -259,7 +260,8 @@ function RSTabs() {
}, []);
const onShowTemplates = useCallback(
() => {
(selectedID?: number) => {
setInsertCstID(selectedID);
setShowTemplates(true);
}, []);
@ -373,6 +375,7 @@ function RSTabs() {
<DlgConstituentaTemplate
schema={schema}
hideWindow={() => setShowTemplates(false)}
insertAfter={insertCstID}
onCreate={handleCreateCst}
/> : null}
<Tabs
@ -412,7 +415,7 @@ function RSTabs() {
</ConceptTab>
</TabList>
<div className='overflow-y-auto' style={{ maxHeight: panelHeight}}>
<div className='overflow-y-auto min-w-[48rem]' style={{ maxHeight: panelHeight}}>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}>
<EditorRSForm
isModified={isModified}
@ -429,7 +432,7 @@ function RSTabs() {
onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst}
onTemplates={() => onShowTemplates()} // TODO: implement insertion point
onTemplates={onShowTemplates}
/>
</TabPanel>
@ -445,6 +448,7 @@ function RSTabs() {
onDeleteCst={promptDeleteCst}
onRenameCst={promptRenameCst}
onEditTerm={promptShowEditTerm}
onTemplates={onShowTemplates}
/>
</TabPanel>

View File

@ -30,7 +30,7 @@ function RSTabsMenu({
const navigate = useNavigate();
const { user } = useAuth();
const {
isOwned, isEditable, isTracking, isReadonly, isClaimable, isForceAdmin,
isOwned, editorMode: isEditable, isTracking, isReadonly, isClaimable, adminMode: isForceAdmin,
toggleForceAdmin, toggleReadonly, processing
} = useRSForm();
const schemaMenu = useDropdown();

View File

@ -59,7 +59,9 @@ export const urls = {
* Global unique IDs.
*/
export const globalIDs = {
main_scroll: 'main-scroll'
main_scroll: 'main-scroll',
library_item_editor: 'library-item-editor',
constituenta_editor: 'constituenta-editor'
}
/**