mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 21:10:38 +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({
|
function SubmitButton({
|
||||||
text = 'ОК', icon, disabled, tooltip, loading,
|
text = 'ОК', icon, disabled, tooltip, loading,
|
||||||
dimensions = 'w-fit h-fit'
|
dimensions = 'w-fit h-fit', ...restProps
|
||||||
}: SubmitButtonProps) {
|
}: SubmitButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button type='submit'
|
<button type='submit'
|
||||||
title={tooltip}
|
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' : ''}`}
|
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}
|
disabled={disabled ?? loading}
|
||||||
|
{...restProps}
|
||||||
>
|
>
|
||||||
{icon ? <span>{icon}</span> : null}
|
{icon ? <span>{icon}</span> : null}
|
||||||
{text ? <span>{text}</span> : null}
|
{text ? <span>{text}</span> : null}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { IConstituenta } from '../../models/rsform';
|
import { IConstituenta } from '../../models/rsform';
|
||||||
import ConceptTooltip from '../Common/ConceptTooltip';
|
import ConceptTooltip from '../Common/ConceptTooltip';
|
||||||
import InfoConstituenta from './InfoConstituenta';
|
import InfoConstituenta from '../Shared/InfoConstituenta';
|
||||||
|
|
||||||
interface ConstituentaTooltipProps {
|
interface ConstituentaTooltipProps {
|
||||||
data: IConstituenta
|
data: IConstituenta
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Divider from '../Common/Divider';
|
import Divider from '../Common/Divider';
|
||||||
import InfoCstStatus from './InfoCstStatus';
|
import InfoCstStatus from '../Shared/InfoCstStatus';
|
||||||
|
|
||||||
function HelpConstituenta() {
|
function HelpConstituenta() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Divider from '../Common/Divider';
|
import Divider from '../Common/Divider';
|
||||||
import InfoCstStatus from './InfoCstStatus';
|
import InfoCstStatus from '../Shared/InfoCstStatus';
|
||||||
|
|
||||||
function HelpRSFormItems() {
|
function HelpRSFormItems() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Divider from '../Common/Divider';
|
import Divider from '../Common/Divider';
|
||||||
import InfoCstClass from './InfoCstClass';
|
import InfoCstClass from '../Shared/InfoCstClass';
|
||||||
import InfoCstStatus from './InfoCstStatus';
|
import InfoCstStatus from '../Shared/InfoCstStatus';
|
||||||
|
|
||||||
function HelpTermGraph() {
|
function HelpTermGraph() {
|
||||||
return (
|
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
|
loading: boolean
|
||||||
processing: boolean
|
processing: boolean
|
||||||
|
|
||||||
|
editorMode: boolean
|
||||||
|
adminMode: boolean
|
||||||
isOwned: boolean
|
isOwned: boolean
|
||||||
isEditable: boolean
|
|
||||||
isClaimable: boolean
|
isClaimable: boolean
|
||||||
isReadonly: boolean
|
isReadonly: boolean
|
||||||
isTracking: boolean
|
isTracking: boolean
|
||||||
isForceAdmin: boolean
|
|
||||||
|
|
||||||
toggleForceAdmin: () => void
|
toggleForceAdmin: () => void
|
||||||
toggleReadonly: () => void
|
toggleReadonly: () => void
|
||||||
|
@ -74,7 +74,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID });
|
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID });
|
||||||
const [ processing, setProcessing ] = useState(false);
|
const [ processing, setProcessing ] = useState(false);
|
||||||
|
|
||||||
const [ isForceAdmin, setIsForceAdmin ] = useState(false);
|
const [ adminMode, setAdminMode ] = useState(false);
|
||||||
const [ isReadonly, setIsReadonly ] = useState(false);
|
const [ isReadonly, setIsReadonly ] = useState(false);
|
||||||
const [ toggleTracking, setToggleTracking ] = 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;
|
return (user?.id !== schema?.owner && schema?.is_common && !schema?.is_canonical) ?? false;
|
||||||
}, [user, schema?.owner, schema?.is_common, schema?.is_canonical]);
|
}, [user, schema?.owner, schema?.is_common, schema?.is_canonical]);
|
||||||
|
|
||||||
const isEditable = useMemo(
|
const editorMode = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return (
|
return (
|
||||||
!loading && !processing && !isReadonly &&
|
!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(
|
const isTracking = useMemo(
|
||||||
() => {
|
() => {
|
||||||
|
@ -322,9 +322,9 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
<RSFormContext.Provider value={{
|
<RSFormContext.Provider value={{
|
||||||
schema,
|
schema,
|
||||||
error, loading, processing,
|
error, loading, processing,
|
||||||
isForceAdmin, isReadonly, isOwned, isEditable,
|
adminMode, isReadonly, isOwned, editorMode,
|
||||||
isClaimable, isTracking,
|
isClaimable, isTracking,
|
||||||
toggleForceAdmin: () => setIsForceAdmin(prev => !prev),
|
toggleForceAdmin: () => setAdminMode(prev => !prev),
|
||||||
toggleReadonly: () => setIsReadonly(prev => !prev),
|
toggleReadonly: () => setIsReadonly(prev => !prev),
|
||||||
update, download, upload, claim, resetAliases, subscribe, unsubscribe,
|
update, download, upload, claim, resetAliases, subscribe, unsubscribe,
|
||||||
cstUpdate, cstCreate, cstRename, cstDelete, cstMoveTo
|
cstUpdate, cstCreate, cstRename, cstDelete, cstMoveTo
|
||||||
|
|
|
@ -18,6 +18,7 @@ interface DlgConstituentaTemplateProps
|
||||||
extends Pick<ModalProps, 'hideWindow'> {
|
extends Pick<ModalProps, 'hideWindow'> {
|
||||||
schema: IRSForm
|
schema: IRSForm
|
||||||
onCreate: (data: ICstCreateData) => void
|
onCreate: (data: ICstCreateData) => void
|
||||||
|
insertAfter?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TabID {
|
export enum TabID {
|
||||||
|
@ -26,7 +27,7 @@ export enum TabID {
|
||||||
CONSTITUENTA = 2
|
CONSTITUENTA = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
function DlgConstituentaTemplate({ hideWindow, schema, onCreate }: DlgConstituentaTemplateProps) {
|
function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }: DlgConstituentaTemplateProps) {
|
||||||
const [ validated, setValidated ] = useState(false);
|
const [ validated, setValidated ] = useState(false);
|
||||||
const [ template, updateTemplate ] = usePartialUpdate<ITemplateState>({});
|
const [ template, updateTemplate ] = usePartialUpdate<ITemplateState>({});
|
||||||
const [ substitutes, updateSubstitutes ] = usePartialUpdate<IArgumentsState>({
|
const [ substitutes, updateSubstitutes ] = usePartialUpdate<IArgumentsState>({
|
||||||
|
@ -35,7 +36,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate }: DlgConstituen
|
||||||
});
|
});
|
||||||
const [constituenta, updateConstituenta] = usePartialUpdate<ICstCreateData>({
|
const [constituenta, updateConstituenta] = usePartialUpdate<ICstCreateData>({
|
||||||
cst_type: CstType.TERM,
|
cst_type: CstType.TERM,
|
||||||
insert_after: null,
|
insert_after: insertAfter ?? null,
|
||||||
alias: '',
|
alias: '',
|
||||||
convention: '',
|
convention: '',
|
||||||
definition_formal: '',
|
definition_formal: '',
|
||||||
|
|
|
@ -87,11 +87,11 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-3'>
|
<div className='flex flex-col gap-3'>
|
||||||
<div>
|
<div>
|
||||||
<div className='flex justify-between gap-6'>
|
<div className='flex justify-between'>
|
||||||
<SelectSingle
|
<SelectSingle
|
||||||
className='w-full'
|
|
||||||
options={categorySelector}
|
|
||||||
placeholder='Выберите категорию'
|
placeholder='Выберите категорию'
|
||||||
|
className='w-full border-none'
|
||||||
|
options={categorySelector}
|
||||||
value={state.filterCategory && selectedSchema ? {
|
value={state.filterCategory && selectedSchema ? {
|
||||||
value: state.filterCategory.id,
|
value: state.filterCategory.id,
|
||||||
label: state.filterCategory.term_raw
|
label: state.filterCategory.term_raw
|
||||||
|
@ -100,9 +100,9 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectSingle
|
<SelectSingle
|
||||||
|
placeholder='Выберите источник'
|
||||||
className='min-w-[15rem]'
|
className='min-w-[15rem]'
|
||||||
options={templateSelector}
|
options={templateSelector}
|
||||||
placeholder='Выберите источник'
|
|
||||||
value={state.templateID ? { value: state.templateID, label: templates.find(item => item.id == state.templateID)!.title }: null}
|
value={state.templateID ? { value: state.templateID, label: templates.find(item => item.id == state.templateID)!.title }: null}
|
||||||
onChange={data => partialUpdate({templateID: (data ? data.value : undefined)})}
|
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"] {
|
[data-color-scheme="dark"] {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
@ -329,6 +336,13 @@
|
||||||
@apply border rounded outline-2 outline
|
@apply border rounded outline-2 outline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-editor .cm-placeholder {
|
||||||
|
color: var(--cl-fg-60);
|
||||||
|
.dark & {
|
||||||
|
color: var(--cd-fg-60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.rdt_TableCell{
|
.rdt_TableCell{
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,14 @@ export interface ILibraryItem {
|
||||||
owner: number | null
|
owner: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents library item extended data.
|
||||||
|
*/
|
||||||
|
export interface ILibraryItemEx
|
||||||
|
extends ILibraryItem {
|
||||||
|
subscribers: number[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents update data for editing {@link ILibraryItem}.
|
* Represents update data for editing {@link ILibraryItem}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Graph } from '../utils/Graph'
|
import { Graph } from '../utils/Graph'
|
||||||
import { ILibraryUpdateData } from './library'
|
import { ILibraryItemEx, ILibraryUpdateData } from './library'
|
||||||
import { ILibraryItem } from './library'
|
|
||||||
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang'
|
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -163,11 +162,10 @@ export interface IRSFormStats {
|
||||||
* Represents formal explication for set of concepts.
|
* Represents formal explication for set of concepts.
|
||||||
*/
|
*/
|
||||||
export interface IRSForm
|
export interface IRSForm
|
||||||
extends ILibraryItem {
|
extends ILibraryItemEx {
|
||||||
items: IConstituenta[]
|
items: IConstituenta[]
|
||||||
stats: IRSFormStats
|
stats: IRSFormStats
|
||||||
graph: Graph
|
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 { Dispatch, SetStateAction, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
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 { useRSForm } from '../../../context/RSFormContext';
|
||||||
import useWindowSize from '../../../hooks/useWindowSize';
|
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 { SyntaxTree } from '../../../models/rslang';
|
||||||
import { labelCstTypification } from '../../../utils/labels';
|
import { globalIDs } from '../../../utils/constants';
|
||||||
import EditorRSExpression from './EditorRSExpression';
|
import ConstituentaToolbar from './ConstituentaToolbar';
|
||||||
|
import FormConstituenta from './FormConstituenta';
|
||||||
import ViewSideConstituents from './ViewSideConstituents';
|
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 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 {
|
interface EditorConstituentaProps {
|
||||||
activeID?: number
|
activeID?: number
|
||||||
activeCst?: IConstituenta | undefined
|
activeCst?: IConstituenta | undefined
|
||||||
|
isModified: boolean
|
||||||
|
setIsModified: Dispatch<SetStateAction<boolean>>
|
||||||
|
|
||||||
onOpenEdit: (cstID: number) => void
|
onOpenEdit: (cstID: number) => void
|
||||||
onShowAST: (expression: string, ast: SyntaxTree) => void
|
onShowAST: (expression: string, ast: SyntaxTree) => void
|
||||||
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
|
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
|
||||||
onRenameCst: (initial: ICstRenameData) => void
|
onRenameCst: (initial: ICstRenameData) => void
|
||||||
onEditTerm: () => void
|
onEditTerm: () => void
|
||||||
onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void
|
onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void
|
||||||
isModified: boolean
|
onTemplates: (insertAfter?: number) => void
|
||||||
setIsModified: Dispatch<SetStateAction<boolean>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorConstituenta({
|
function EditorConstituenta({
|
||||||
isModified, setIsModified, activeID, activeCst, onEditTerm,
|
isModified, setIsModified, activeID, activeCst, onEditTerm,
|
||||||
onShowAST, onCreateCst, onRenameCst, onOpenEdit, onDeleteCst
|
onShowAST, onCreateCst, onRenameCst, onOpenEdit, onDeleteCst, onTemplates
|
||||||
}: EditorConstituentaProps) {
|
}: EditorConstituentaProps) {
|
||||||
const windowSize = useWindowSize();
|
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 [toggleReset, setToggleReset] = useState(false);
|
||||||
|
|
||||||
const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]);
|
const readyForEdit = useMemo(() => (!!activeCst && editorMode), [activeCst, editorMode]);
|
||||||
|
|
||||||
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('Изменения сохранены'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
if (!schema || !activeID) {
|
if (!schema || !activeID) {
|
||||||
|
@ -139,22 +82,16 @@ function EditorConstituenta({
|
||||||
onCreateCst(data, true);
|
onCreateCst(data, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRename() {
|
function initiateSubmit() {
|
||||||
if (!activeID || !activeCst) {
|
const element = document.getElementById(globalIDs.constituenta_editor) as HTMLFormElement;
|
||||||
return;
|
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>) {
|
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (event.ctrlKey && event.code === 'KeyS') {
|
if (event.ctrlKey && event.code === 'KeyS') {
|
||||||
if (isModified) {
|
if (isModified) {
|
||||||
handleSubmit();
|
initiateSubmit();
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -162,144 +99,44 @@ function EditorConstituenta({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1}
|
<div tabIndex={-1}
|
||||||
className='flex max-w-[1500px]'
|
className='max-w-[1500px]'
|
||||||
onKeyDown={handleInput}
|
onKeyDown={handleInput}
|
||||||
>
|
>
|
||||||
<form
|
<ConstituentaToolbar
|
||||||
onSubmit={handleSubmit}
|
editorMode={readyForEdit}
|
||||||
className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'
|
isModified={isModified}
|
||||||
>
|
|
||||||
<div className='relative w-full'>
|
onSubmit={initiateSubmit}
|
||||||
<div className='absolute top-0 right-0 flex items-start justify-between w-full'>
|
onReset={() => setToggleReset(prev => !prev)}
|
||||||
{activeCst &&
|
|
||||||
<MiniButton
|
onDelete={handleDelete}
|
||||||
tooltip={`Редактировать словоформы термина: ${activeCst.term_forms.length}`}
|
onClone={handleCloneCst}
|
||||||
disabled={!isEnabled}
|
onCreate={handleCreateCst}
|
||||||
dimensions='w-fit ml-[3.2rem] pt-[0.3rem]'
|
onTemplates={() => onTemplates(activeID)}
|
||||||
noHover
|
/>
|
||||||
onClick={onEditTerm}
|
<div className='flex justify-start'>
|
||||||
icon={<EditIcon size={4} color={isEnabled ? 'text-primary' : ''} />}
|
<div className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'>
|
||||||
/>}
|
<FormConstituenta id={globalIDs.constituenta_editor}
|
||||||
<div className='flex items-center justify-center w-full pl-[4rem]'>
|
constituenta={activeCst}
|
||||||
<div className='font-semibold pointer-events-none w-fit'>
|
isModified={isModified}
|
||||||
<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}
|
|
||||||
toggleReset={toggleReset}
|
toggleReset={toggleReset}
|
||||||
|
|
||||||
|
setIsModified={setIsModified}
|
||||||
onShowAST={onShowAST}
|
onShowAST={onShowAST}
|
||||||
onChange={newValue => setExpression(newValue)}
|
onEditTerm={onEditTerm}
|
||||||
setTypification={setTypification}
|
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>
|
</div>
|
||||||
</form>
|
{(windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD) ?
|
||||||
{(windowSize.width ?? 0) >= SIDELIST_HIDE_THRESHOLD ?
|
<div className='w-full mt-[2.25rem] border h-fit'>
|
||||||
<div className='w-full mt-[2.25rem] border h-fit'>
|
<ViewSideConstituents
|
||||||
<ViewSideConstituents
|
expression={activeCst?.definition_formal ?? ''}
|
||||||
expression={expression}
|
baseHeight={UNFOLDED_HEIGHT}
|
||||||
baseHeight={UNFOLDED_HEIGHT}
|
activeID={activeID}
|
||||||
activeID={activeID}
|
onOpenEdit={onOpenEdit}
|
||||||
onOpenEdit={onOpenEdit}
|
/>
|
||||||
/>
|
</div> : null}
|
||||||
</div> : null}
|
</div>
|
||||||
</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 { Dispatch, SetStateAction } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
import Checkbox from '../../../components/Common/Checkbox';
|
|
||||||
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
|
|
||||||
import Divider from '../../../components/Common/Divider';
|
import Divider from '../../../components/Common/Divider';
|
||||||
import MiniButton from '../../../components/Common/MiniButton';
|
import InfoLibraryItem from '../../../components/Shared/InfoLibraryItem';
|
||||||
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 { useAuth } from '../../../context/AuthContext';
|
import { useAuth } from '../../../context/AuthContext';
|
||||||
import { useRSForm } from '../../../context/RSFormContext';
|
import { useRSForm } from '../../../context/RSFormContext';
|
||||||
import { useUsers } from '../../../context/UsersContext';
|
import { globalIDs } from '../../../utils/constants';
|
||||||
import { LibraryItemType } from '../../../models/library';
|
import FormRSForm from './FormRSForm';
|
||||||
import { IRSFormCreateData } from '../../../models/rsform';
|
|
||||||
import RSFormStats from './RSFormStats';
|
import RSFormStats from './RSFormStats';
|
||||||
|
import RSFormToolbar from './RSFormToolbar';
|
||||||
|
|
||||||
interface EditorRSFormProps {
|
interface EditorRSFormProps {
|
||||||
onDestroy: () => void
|
onDestroy: () => void
|
||||||
|
@ -28,66 +19,20 @@ interface EditorRSFormProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified, onDownload }: EditorRSFormProps) {
|
function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified, onDownload }: EditorRSFormProps) {
|
||||||
const intl = useIntl();
|
const { schema, editorMode: isEditable, isClaimable } = useRSForm();
|
||||||
const { getUserLabel } = useUsers();
|
|
||||||
const {
|
|
||||||
schema, update, isForceAdmin,
|
|
||||||
isEditable, isClaimable, processing
|
|
||||||
} = useRSForm();
|
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const [title, setTitle] = useState('');
|
function initiateSubmit() {
|
||||||
const [alias, setAlias] = useState('');
|
const element = document.getElementById(globalIDs.library_item_editor) as HTMLFormElement;
|
||||||
const [comment, setComment] = useState('');
|
if (element) {
|
||||||
const [common, setCommon] = useState(false);
|
element.requestSubmit();
|
||||||
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('Изменения сохранены'));
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (event.ctrlKey && event.code === 'KeyS') {
|
if (event.ctrlKey && event.code === 'KeyS') {
|
||||||
if (isModified) {
|
if (isModified) {
|
||||||
handleSubmit();
|
initiateSubmit();
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -95,116 +40,31 @@ function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified,
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} onKeyDown={handleInput}>
|
<div tabIndex={-1} onKeyDown={handleInput}>
|
||||||
<div className='relative flex items-start justify-center w-full'>
|
<RSFormToolbar
|
||||||
<div className='absolute flex mt-1'>
|
editorMode={isEditable}
|
||||||
<MiniButton
|
modified={isModified}
|
||||||
tooltip='Сохранить изменения'
|
claimable={isClaimable}
|
||||||
disabled={!isModified || !isEditable}
|
anonymous={!user}
|
||||||
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>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-1'>
|
onSubmit={initiateSubmit}
|
||||||
<div className='flex justify-start'>
|
onShare={onShare}
|
||||||
<label className='font-semibold'>Владелец:</label>
|
onDownload={onDownload}
|
||||||
<span className='min-w-[200px] ml-2 overflow-ellipsis overflow-hidden whitespace-nowrap'>
|
onClaim={onClaim}
|
||||||
{getUserLabel(schema?.owner ?? null)}
|
onDestroy={onDestroy}
|
||||||
</span>
|
/>
|
||||||
</div>
|
<div className='flex w-full'>
|
||||||
<div className='flex justify-start'>
|
<div className='flex-grow max-w-[40rem] min-w-[30rem] px-4 py-2'>
|
||||||
<label className='font-semibold'>Отслеживают:</label>
|
<div className='flex flex-col gap-3 mt-2'>
|
||||||
<span id='subscriber-count' className='ml-2'>
|
<FormRSForm id={globalIDs.library_item_editor}
|
||||||
{ schema?.subscribers.length ?? 0 }
|
isModified={isModified}
|
||||||
</span>
|
setIsModified={setIsModified}
|
||||||
</div>
|
/>
|
||||||
<div className='flex justify-start'>
|
|
||||||
<label className='font-semibold'>Дата обновления:</label>
|
<Divider margins='my-2' />
|
||||||
<span className='ml-2'>{schema && new Date(schema?.time_update).toLocaleString(intl.locale)}</span>
|
|
||||||
</div>
|
<InfoLibraryItem item={schema} />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
<Divider vertical />
|
<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 {
|
interface EditorRSListProps {
|
||||||
onOpenEdit: (cstID: number) => void
|
onOpenEdit: (cstID: number) => void
|
||||||
onTemplates: (selected: number[]) => void
|
onTemplates: (insertAfter?: number) => void
|
||||||
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
|
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
|
||||||
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
|
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ interface EditorRSListProps {
|
||||||
function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: EditorRSListProps) {
|
function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: EditorRSListProps) {
|
||||||
const { colors, mainHeight } = useConceptTheme();
|
const { colors, mainHeight } = useConceptTheme();
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm();
|
const { schema, editorMode: isEditable, cstMoveTo, resetAliases } = useRSForm();
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
@ -307,7 +307,7 @@ function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edi
|
||||||
onClone={handleClone}
|
onClone={handleClone}
|
||||||
onCreate={handleCreateCst}
|
onCreate={handleCreateCst}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onTemplates={() => onTemplates(selected)}
|
onTemplates={() => onTemplates(selected.length !== 0 ? selected[selected.length-1] : undefined)}
|
||||||
onReindex={handleReindex}
|
onReindex={handleReindex}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -88,14 +88,12 @@ function RSListToolbar({
|
||||||
})}
|
})}
|
||||||
</Dropdown>}
|
</Dropdown>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
tooltip='Создать конституенту из шаблона'
|
tooltip='Создать конституенту из шаблона'
|
||||||
icon={<DiamondIcon color={editorMode ? 'text-primary': ''} size={5}/>}
|
icon={<DiamondIcon color={editorMode ? 'text-primary': ''} size={5}/>}
|
||||||
disabled={!editorMode}
|
disabled={!editorMode}
|
||||||
onClick={onTemplates}
|
onClick={onTemplates}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
tooltip='Сброс имен: присвоить порядковые имена'
|
tooltip='Сброс имен: присвоить порядковые имена'
|
||||||
icon={<UpdateIcon color={editorMode ? 'text-primary': ''} size={5}/>}
|
icon={<UpdateIcon color={editorMode ? 'text-primary': ''} size={5}/>}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { GraphEdge, GraphNode, LayoutTypes } from 'reagraph';
|
import { GraphEdge, GraphNode, LayoutTypes } from 'reagraph';
|
||||||
|
|
||||||
import InfoConstituenta from '../../../components/Help/InfoConstituenta';
|
import InfoConstituenta from '../../../components/Shared/InfoConstituenta';
|
||||||
import { useRSForm } from '../../../context/RSFormContext';
|
import { useRSForm } from '../../../context/RSFormContext';
|
||||||
import { useConceptTheme } from '../../../context/ThemeContext';
|
import { useConceptTheme } from '../../../context/ThemeContext';
|
||||||
import DlgGraphParams from '../../../dialogs/DlgGraphParams';
|
import DlgGraphParams from '../../../dialogs/DlgGraphParams';
|
||||||
|
@ -12,6 +12,7 @@ import { colorbgGraphNode } from '../../../utils/color';
|
||||||
import { TIMEOUT_GRAPH_REFRESH } from '../../../utils/constants';
|
import { TIMEOUT_GRAPH_REFRESH } from '../../../utils/constants';
|
||||||
import GraphSidebar from './GraphSidebar';
|
import GraphSidebar from './GraphSidebar';
|
||||||
import GraphToolbar from './GraphToolbar';
|
import GraphToolbar from './GraphToolbar';
|
||||||
|
import SelectedCounter from './SelectedCounter';
|
||||||
import TermGraph from './TermGraph';
|
import TermGraph from './TermGraph';
|
||||||
import useGraphFilter from './useGraphFilter';
|
import useGraphFilter from './useGraphFilter';
|
||||||
import ViewHidden from './ViewHidden';
|
import ViewHidden from './ViewHidden';
|
||||||
|
@ -23,7 +24,7 @@ interface EditorTermGraphProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) {
|
function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) {
|
||||||
const { schema, isEditable } = useRSForm();
|
const { schema, editorMode: isEditable } = useRSForm();
|
||||||
const { colors } = useConceptTheme();
|
const { colors } = useConceptTheme();
|
||||||
|
|
||||||
const [toggleDataUpdate, setToggleDataUpdate] = useState(false);
|
const [toggleDataUpdate, setToggleDataUpdate] = useState(false);
|
||||||
|
@ -196,12 +197,10 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
||||||
onConfirm={handleChangeParams}
|
onConfirm={handleChangeParams}
|
||||||
/> : null}
|
/> : null}
|
||||||
|
|
||||||
{selected.length > 0 ?
|
<SelectedCounter
|
||||||
<div className='relative w-full z-pop'>
|
total={schema?.stats?.count_all ?? 0}
|
||||||
<div className='absolute top-0 left-0 px-2 select-none whitespace-nowrap small-caps clr-app'>
|
selected={selected.length}
|
||||||
Выбор {selected.length} из {schema?.stats?.count_all ?? 0}
|
/>
|
||||||
</div>
|
|
||||||
</div> : null}
|
|
||||||
|
|
||||||
<GraphToolbar
|
<GraphToolbar
|
||||||
editorMode={isEditable}
|
editorMode={isEditable}
|
||||||
|
@ -233,7 +232,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
||||||
</div> : null}
|
</div> : null}
|
||||||
|
|
||||||
<div className='relative z-pop'>
|
<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
|
<GraphSidebar
|
||||||
coloring={coloringScheme}
|
coloring={coloringScheme}
|
||||||
layout={layout}
|
layout={layout}
|
||||||
|
|
|
@ -18,19 +18,17 @@ function GraphSidebar({
|
||||||
layout, setLayout
|
layout, setLayout
|
||||||
} : GraphSidebarProps) {
|
} : GraphSidebarProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col px-2 pb-2 mt-8 text-sm select-none h-fit'>
|
<div className='flex flex-col px-2 pb-2 text-sm select-none mt-9 h-fit'>
|
||||||
<div className='flex items-center w-full gap-1 text-sm'>
|
<SelectSingle
|
||||||
<SelectSingle
|
placeholder='Выберите цвет'
|
||||||
placeholder='Выберите цвет'
|
options={SelectorGraphColoring}
|
||||||
options={SelectorGraphColoring}
|
isSearchable={false}
|
||||||
isSearchable={false}
|
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
||||||
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
||||||
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<SelectSingle
|
<SelectSingle
|
||||||
placeholder='Способ расположения'
|
placeholder='Способ расположения'
|
||||||
className='w-full mt-1'
|
className='w-full'
|
||||||
options={SelectorGraphLayout}
|
options={SelectorGraphLayout}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
value={layout ? { value: layout, label: mapLableLayout.get(layout) } : null}
|
value={layout ? { value: layout, label: mapLableLayout.get(layout) } : null}
|
||||||
|
|
|
@ -29,47 +29,47 @@ function GraphToolbar({
|
||||||
} : GraphToolbarProps) {
|
} : GraphToolbarProps) {
|
||||||
return (
|
return (
|
||||||
<div className='relative w-full z-pop'>
|
<div className='relative w-full z-pop'>
|
||||||
<div className='absolute right-0 flex items-start justify-center w-full top-1'>
|
<div className='absolute right-0 flex items-start justify-center w-full top-1'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
tooltip='Настройки фильтрации узлов и связей'
|
tooltip='Настройки фильтрации узлов и связей'
|
||||||
icon={<FilterIcon color='text-primary' size={5} />}
|
icon={<FilterIcon color='text-primary' size={5} />}
|
||||||
onClick={showParamsDialog} />
|
onClick={showParamsDialog} />
|
||||||
<MiniButton
|
<MiniButton
|
||||||
tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'}
|
tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'}
|
||||||
icon={
|
icon={
|
||||||
!noText
|
!noText
|
||||||
? <LetterALinesIcon color='text-success' size={5} />
|
? <LetterALinesIcon color='text-success' size={5} />
|
||||||
: <LetterAIcon color='text-primary' size={5} />
|
: <LetterAIcon color='text-primary' size={5} />
|
||||||
}
|
}
|
||||||
onClick={toggleNoText} />
|
onClick={toggleNoText} />
|
||||||
<MiniButton
|
<MiniButton
|
||||||
tooltip='Новая конституента'
|
tooltip='Новая конституента'
|
||||||
icon={<SmallPlusIcon color={editorMode ? 'text-success' : ''} size={5} />}
|
icon={<SmallPlusIcon color={editorMode ? 'text-success' : ''} size={5} />}
|
||||||
disabled={!editorMode}
|
disabled={!editorMode}
|
||||||
onClick={onCreate} />
|
onClick={onCreate} />
|
||||||
<MiniButton
|
<MiniButton
|
||||||
tooltip='Удалить выбранные'
|
tooltip='Удалить выбранные'
|
||||||
icon={<DumpBinIcon color={editorMode && !nothingSelected ? 'text-warning' : ''} size={5} />}
|
icon={<DumpBinIcon color={editorMode && !nothingSelected ? 'text-warning' : ''} size={5} />}
|
||||||
disabled={!editorMode || nothingSelected}
|
disabled={!editorMode || nothingSelected}
|
||||||
onClick={onDelete} />
|
onClick={onDelete} />
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<ArrowsFocusIcon color='text-primary' size={5} />}
|
icon={<ArrowsFocusIcon color='text-primary' size={5} />}
|
||||||
tooltip='Восстановить камеру'
|
tooltip='Восстановить камеру'
|
||||||
onClick={onResetViewpoint} />
|
onClick={onResetViewpoint} />
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<PlanetIcon color={!is3D ? '' : orbit ? 'text-success' : 'text-primary'} size={5} />}
|
icon={<PlanetIcon color={!is3D ? '' : orbit ? 'text-success' : 'text-primary'} size={5} />}
|
||||||
tooltip='Анимация вращения'
|
tooltip='Анимация вращения'
|
||||||
disabled={!is3D}
|
disabled={!is3D}
|
||||||
onClick={toggleOrbit} />
|
onClick={toggleOrbit} />
|
||||||
<div className='px-1 py-1' id='items-graph-help'>
|
<div className='px-1 py-1' id='items-graph-help'>
|
||||||
<HelpIcon color='text-primary' size={5} />
|
<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>
|
||||||
|
<ConceptTooltip anchorSelect='#items-graph-help'>
|
||||||
|
<div className='text-sm max-w-[calc(100vw-20rem)] z-tooltip'>
|
||||||
|
<HelpTermGraph />
|
||||||
|
</div>
|
||||||
|
</ConceptTooltip>
|
||||||
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 [showEditTerm, setShowEditTerm] = useState(false);
|
||||||
|
|
||||||
|
const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined);
|
||||||
const [showTemplates, setShowTemplates] = useState(false);
|
const [showTemplates, setShowTemplates] = useState(false);
|
||||||
|
|
||||||
const panelHeight = useMemo(
|
const panelHeight = useMemo(
|
||||||
|
@ -259,7 +260,8 @@ function RSTabs() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onShowTemplates = useCallback(
|
const onShowTemplates = useCallback(
|
||||||
() => {
|
(selectedID?: number) => {
|
||||||
|
setInsertCstID(selectedID);
|
||||||
setShowTemplates(true);
|
setShowTemplates(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -373,6 +375,7 @@ function RSTabs() {
|
||||||
<DlgConstituentaTemplate
|
<DlgConstituentaTemplate
|
||||||
schema={schema}
|
schema={schema}
|
||||||
hideWindow={() => setShowTemplates(false)}
|
hideWindow={() => setShowTemplates(false)}
|
||||||
|
insertAfter={insertCstID}
|
||||||
onCreate={handleCreateCst}
|
onCreate={handleCreateCst}
|
||||||
/> : null}
|
/> : null}
|
||||||
<Tabs
|
<Tabs
|
||||||
|
@ -412,7 +415,7 @@ function RSTabs() {
|
||||||
</ConceptTab>
|
</ConceptTab>
|
||||||
</TabList>
|
</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' }}>
|
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}>
|
||||||
<EditorRSForm
|
<EditorRSForm
|
||||||
isModified={isModified}
|
isModified={isModified}
|
||||||
|
@ -429,7 +432,7 @@ function RSTabs() {
|
||||||
onOpenEdit={onOpenCst}
|
onOpenEdit={onOpenCst}
|
||||||
onCreateCst={promptCreateCst}
|
onCreateCst={promptCreateCst}
|
||||||
onDeleteCst={promptDeleteCst}
|
onDeleteCst={promptDeleteCst}
|
||||||
onTemplates={() => onShowTemplates()} // TODO: implement insertion point
|
onTemplates={onShowTemplates}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
|
@ -445,6 +448,7 @@ function RSTabs() {
|
||||||
onDeleteCst={promptDeleteCst}
|
onDeleteCst={promptDeleteCst}
|
||||||
onRenameCst={promptRenameCst}
|
onRenameCst={promptRenameCst}
|
||||||
onEditTerm={promptShowEditTerm}
|
onEditTerm={promptShowEditTerm}
|
||||||
|
onTemplates={onShowTemplates}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ function RSTabsMenu({
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const {
|
const {
|
||||||
isOwned, isEditable, isTracking, isReadonly, isClaimable, isForceAdmin,
|
isOwned, editorMode: isEditable, isTracking, isReadonly, isClaimable, adminMode: isForceAdmin,
|
||||||
toggleForceAdmin, toggleReadonly, processing
|
toggleForceAdmin, toggleReadonly, processing
|
||||||
} = useRSForm();
|
} = useRSForm();
|
||||||
const schemaMenu = useDropdown();
|
const schemaMenu = useDropdown();
|
||||||
|
|
|
@ -59,7 +59,9 @@ export const urls = {
|
||||||
* Global unique IDs.
|
* Global unique IDs.
|
||||||
*/
|
*/
|
||||||
export const globalIDs = {
|
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