mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Rework editor forms
This commit is contained in:
parent
4b77faf98b
commit
29691e9f6a
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Divider from '../Common/Divider';
|
||||
import InfoCstStatus from './InfoCstStatus';
|
||||
import InfoCstStatus from '../Shared/InfoCstStatus';
|
||||
|
||||
function HelpConstituenta() {
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Divider from '../Common/Divider';
|
||||
import InfoCstStatus from './InfoCstStatus';
|
||||
import InfoCstStatus from '../Shared/InfoCstStatus';
|
||||
|
||||
function HelpRSFormItems() {
|
||||
return (
|
||||
|
|
|
@ -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 (
|
||||
|
|
38
rsconcept/frontend/src/components/Shared/InfoLibraryItem.tsx
Normal file
38
rsconcept/frontend/src/components/Shared/InfoLibraryItem.tsx
Normal 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;
|
|
@ -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
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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)})}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}.
|
||||
*/
|
||||
|
|
|
@ -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[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
|
@ -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' : ''} />}
|
||||
<ConstituentaToolbar
|
||||
editorMode={readyForEdit}
|
||||
isModified={isModified}
|
||||
|
||||
onSubmit={initiateSubmit}
|
||||
onReset={() => setToggleReset(prev => !prev)}
|
||||
|
||||
onDelete={handleDelete}
|
||||
onClone={handleCloneCst}
|
||||
onCreate={handleCreateCst}
|
||||
onTemplates={() => onTemplates(activeID)}
|
||||
/>
|
||||
</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}
|
||||
<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}
|
||||
/>
|
||||
<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} />}
|
||||
onEditTerm={onEditTerm}
|
||||
onRenameCst={onRenameCst}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{(windowSize.width ?? 0) >= SIDELIST_HIDE_THRESHOLD ?
|
||||
{(windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD) ?
|
||||
<div className='w-full mt-[2.25rem] border h-fit'>
|
||||
<ViewSideConstituents
|
||||
expression={expression}
|
||||
expression={activeCst?.definition_formal ?? ''}
|
||||
baseHeight={UNFOLDED_HEIGHT}
|
||||
activeID={activeID}
|
||||
onOpenEdit={onOpenEdit}
|
||||
/>
|
||||
</div> : null}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Divider vertical />
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
|
|
|
@ -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}/>}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -18,8 +18,7 @@ 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'>
|
||||
<div className='flex flex-col px-2 pb-2 text-sm select-none mt-9 h-fit'>
|
||||
<SelectSingle
|
||||
placeholder='Выберите цвет'
|
||||
options={SelectorGraphColoring}
|
||||
|
@ -27,10 +26,9 @@ function GraphSidebar({
|
|||
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
||||
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
||||
/>
|
||||
</div>
|
||||
<SelectSingle
|
||||
placeholder='Способ расположения'
|
||||
className='w-full mt-1'
|
||||
className='w-full'
|
||||
options={SelectorGraphLayout}
|
||||
isSearchable={false}
|
||||
value={layout ? { value: layout, label: mapLableLayout.get(layout) } : null}
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue
Block a user