mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Implement constituenta substitution and small UI fixes
This commit is contained in:
parent
52ed20942e
commit
95dc3d4b9b
48
rsconcept/frontend/src/components/ConstituentaSelector.tsx
Normal file
48
rsconcept/frontend/src/components/ConstituentaSelector.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { CstMatchMode } from '@/models/miscellaneous';
|
||||
import { EntityID, IConstituenta } from '@/models/rsform';
|
||||
import { matchConstituenta } from '@/models/rsformAPI';
|
||||
import { describeConstituenta, describeConstituentaTerm } from '@/utils/labels';
|
||||
|
||||
import SelectSingle from './ui/SelectSingle';
|
||||
|
||||
interface ConstituentaSelectorProps {
|
||||
items?: IConstituenta[];
|
||||
value?: IConstituenta;
|
||||
onSelectValue: (newValue?: IConstituenta) => void;
|
||||
}
|
||||
|
||||
function ConstituentaSelector({ items, value, onSelectValue }: ConstituentaSelectorProps) {
|
||||
const options = useMemo(() => {
|
||||
return (
|
||||
items?.map(cst => ({
|
||||
value: cst.id,
|
||||
label: `${cst.alias}: ${describeConstituenta(cst)}`
|
||||
})) ?? []
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
const filter = useCallback(
|
||||
(option: { value: EntityID | undefined; label: string }, inputValue: string) => {
|
||||
const cst = items?.find(item => item.id === option.value);
|
||||
return !cst ? false : matchConstituenta(cst, inputValue, CstMatchMode.ALL);
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectSingle
|
||||
className='w-[20rem] text-ellipsis'
|
||||
options={options}
|
||||
value={{ value: value?.id, label: value ? `${value.alias}: ${describeConstituentaTerm(value)}` : '' }}
|
||||
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
|
||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||
filterOption={filter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConstituentaSelector;
|
|
@ -32,9 +32,9 @@ function Tooltip({
|
|||
delayShow={1000}
|
||||
delayHide={100}
|
||||
opacity={0.97}
|
||||
className={clsx('overflow-hidden', 'border shadow-md', layer, className)}
|
||||
className={clsx('overflow-auto sm:overflow-hidden', 'border shadow-md', layer, className)}
|
||||
classNameArrow={layer}
|
||||
style={{ ...{ paddingTop: '2px', paddingBottom: '2px', overflowX: 'auto', overflowY: 'auto' }, ...style }}
|
||||
style={{ ...{ paddingTop: '2px', paddingBottom: '2px' }, ...style }}
|
||||
variant={darkMode ? 'dark' : 'light'}
|
||||
place={place}
|
||||
{...restProps}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
ICstCreateData,
|
||||
ICstMovetoData,
|
||||
ICstRenameData,
|
||||
ICstSubstituteData,
|
||||
ICstUpdateData,
|
||||
IRSForm,
|
||||
IRSFormUploadData
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
patchMoveConstituenta,
|
||||
patchRenameConstituenta,
|
||||
patchResetAliases,
|
||||
patchSubstituteConstituenta,
|
||||
patchUploadTRS,
|
||||
postClaimLibraryItem,
|
||||
postNewConstituenta,
|
||||
|
@ -57,6 +59,7 @@ interface IRSFormContext {
|
|||
|
||||
cstCreate: (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => void;
|
||||
cstRename: (data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => void;
|
||||
cstSubstitute: (data: ICstSubstituteData, callback?: () => void) => void;
|
||||
cstUpdate: (data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => void;
|
||||
cstDelete: (data: IConstituentaList, callback?: () => void) => void;
|
||||
cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void;
|
||||
|
@ -320,6 +323,24 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
[setError, setSchema, library, schemaID]
|
||||
);
|
||||
|
||||
const cstSubstitute = useCallback(
|
||||
(data: ICstSubstituteData, callback?: () => void) => {
|
||||
setError(undefined);
|
||||
patchSubstituteConstituenta(schemaID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setError,
|
||||
onSuccess: newData => {
|
||||
setSchema(newData);
|
||||
library.localUpdateTimestamp(newData.id);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
[setError, setSchema, library, schemaID]
|
||||
);
|
||||
|
||||
const cstMoveTo = useCallback(
|
||||
(data: ICstMovetoData, callback?: () => void) => {
|
||||
setError(undefined);
|
||||
|
@ -358,6 +379,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
cstUpdate,
|
||||
cstCreate,
|
||||
cstRename,
|
||||
cstSubstitute,
|
||||
cstDelete,
|
||||
cstMoveTo
|
||||
}}
|
||||
|
|
70
rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx
Normal file
70
rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { LuReplace } from 'react-icons/lu';
|
||||
|
||||
import ConstituentaSelector from '@/components/ConstituentaSelector';
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import FlexColumn from '@/components/ui/FlexColumn';
|
||||
import Label from '@/components/ui/Label';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { IConstituenta, ICstSubstituteData } from '@/models/rsform';
|
||||
|
||||
interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
onSubstitute: (data: ICstSubstituteData) => void;
|
||||
}
|
||||
|
||||
function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
|
||||
const { schema } = useRSForm();
|
||||
|
||||
const [original, setOriginal] = useState<IConstituenta | undefined>(undefined);
|
||||
const [substitution, setSubstitution] = useState<IConstituenta | undefined>(undefined);
|
||||
const [transferTerm, setTransferTerm] = useState(false);
|
||||
|
||||
const canSubmit = useMemo(() => {
|
||||
return !!original && !!substitution && substitution.id !== original.id;
|
||||
}, [original, substitution]);
|
||||
|
||||
function handleSubmit() {
|
||||
const data: ICstSubstituteData = {
|
||||
original: original!.id,
|
||||
substitution: substitution!.id,
|
||||
transfer_term: transferTerm
|
||||
};
|
||||
onSubstitute(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header='Отождествление конституенты'
|
||||
submitText='Отождествить'
|
||||
submitInvalidTooltip={'Выберите две различные конституенты'}
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={canSubmit}
|
||||
onSubmit={handleSubmit}
|
||||
className={clsx('w-[30rem]', 'px-6 py-3 flex flex-col gap-3 justify-center items-center')}
|
||||
>
|
||||
<FlexColumn>
|
||||
<Label text='Удаляемая конституента' />
|
||||
<ConstituentaSelector items={schema?.items} value={original} onSelectValue={setOriginal} />
|
||||
</FlexColumn>
|
||||
<div className=''>
|
||||
<LuReplace size='3rem' className='clr-text-primary' />
|
||||
</div>
|
||||
<FlexColumn>
|
||||
<Label text='Подставляемая конституента' />
|
||||
<ConstituentaSelector items={schema?.items} value={substitution} onSelectValue={setSubstitution} />
|
||||
</FlexColumn>
|
||||
<Checkbox
|
||||
className='mt-3'
|
||||
label='Сохранить термин удаляемой конституенты'
|
||||
value={transferTerm}
|
||||
setValue={setTransferTerm}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DlgSubstituteCst;
|
|
@ -24,6 +24,11 @@ export enum CstType {
|
|||
// CstType constant for category dividers in TemplateSchemas. TODO: create separate structure for templates
|
||||
export const CATEGORY_CST_TYPE = CstType.THEOREM;
|
||||
|
||||
/**
|
||||
* Represents Entity identifier type.
|
||||
*/
|
||||
export type EntityID = number;
|
||||
|
||||
/**
|
||||
* Represents Constituenta classification in terms of system of concepts.
|
||||
*/
|
||||
|
@ -58,7 +63,7 @@ export interface TermForm {
|
|||
* Represents Constituenta basic persistent data.
|
||||
*/
|
||||
export interface IConstituentaMeta {
|
||||
id: number;
|
||||
id: EntityID;
|
||||
schema: number;
|
||||
order: number;
|
||||
alias: string;
|
||||
|
@ -130,6 +135,15 @@ export interface ICstUpdateData
|
|||
*/
|
||||
export interface ICstRenameData extends Pick<IConstituentaMeta, 'id' | 'alias' | 'cst_type'> {}
|
||||
|
||||
/**
|
||||
* Represents data, used in merging {@link IConstituenta}.
|
||||
*/
|
||||
export interface ICstSubstituteData {
|
||||
original: EntityID;
|
||||
substitution: EntityID;
|
||||
transfer_term: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data response when creating {@link IConstituenta}.
|
||||
*/
|
||||
|
|
|
@ -18,6 +18,7 @@ import DlgCreateCst from '@/dialogs/DlgCreateCst';
|
|||
import DlgDeleteCst from '@/dialogs/DlgDeleteCst';
|
||||
import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
|
||||
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
||||
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
||||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||
import { UserAccessMode } from '@/models/miscellaneous';
|
||||
import {
|
||||
|
@ -27,6 +28,7 @@ import {
|
|||
ICstCreateData,
|
||||
ICstMovetoData,
|
||||
ICstRenameData,
|
||||
ICstSubstituteData,
|
||||
ICstUpdateData,
|
||||
IRSForm,
|
||||
TermForm
|
||||
|
@ -54,6 +56,7 @@ interface IRSEditContext {
|
|||
toggleSubscribe: () => void;
|
||||
download: () => void;
|
||||
reindex: () => void;
|
||||
substitute: () => void;
|
||||
}
|
||||
|
||||
const RSEditContext = createContext<IRSEditContext | null>(null);
|
||||
|
@ -100,8 +103,9 @@ export const RSEditState = ({
|
|||
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [showClone, setShowClone] = useState(false);
|
||||
|
||||
const [showDeleteCst, setShowDeleteCst] = useState(false);
|
||||
const [showEditTerm, setShowEditTerm] = useState(false);
|
||||
const [showSubstitute, setShowSubstitute] = useState(false);
|
||||
|
||||
const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
|
||||
const [showCreateCst, setShowCreateCst] = useState(false);
|
||||
|
@ -109,8 +113,6 @@ export const RSEditState = ({
|
|||
const [renameInitialData, setRenameInitialData] = useState<ICstRenameData>();
|
||||
const [showRenameCst, setShowRenameCst] = useState(false);
|
||||
|
||||
const [showEditTerm, setShowEditTerm] = useState(false);
|
||||
|
||||
const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined);
|
||||
const [showTemplates, setShowTemplates] = useState(false);
|
||||
|
||||
|
@ -150,6 +152,13 @@ export const RSEditState = ({
|
|||
[model, renameInitialData]
|
||||
);
|
||||
|
||||
const handleSubstituteCst = useCallback(
|
||||
(data: ICstSubstituteData) => {
|
||||
model.cstSubstitute(data, () => toast.success('Отождествление завершено'));
|
||||
},
|
||||
[model]
|
||||
);
|
||||
|
||||
const handleDeleteCst = useCallback(
|
||||
(deleted: number[]) => {
|
||||
if (!model.schema) {
|
||||
|
@ -282,6 +291,10 @@ export const RSEditState = ({
|
|||
setShowRenameCst(true);
|
||||
}, [activeCst]);
|
||||
|
||||
const substitute = useCallback(() => {
|
||||
setShowSubstitute(true);
|
||||
}, []);
|
||||
|
||||
const editTermForms = useCallback(() => {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
|
@ -370,7 +383,8 @@ export const RSEditState = ({
|
|||
claim,
|
||||
share,
|
||||
toggleSubscribe,
|
||||
reindex
|
||||
reindex,
|
||||
substitute
|
||||
}}
|
||||
>
|
||||
{model.schema ? (
|
||||
|
@ -392,6 +406,12 @@ export const RSEditState = ({
|
|||
initial={renameInitialData}
|
||||
/>
|
||||
) : null}
|
||||
{showSubstitute ? (
|
||||
<DlgSubstituteCst
|
||||
hideWindow={() => setShowSubstitute(false)} // prettier: split lines
|
||||
onSubstitute={handleSubstituteCst}
|
||||
/>
|
||||
) : null}
|
||||
{showDeleteCst ? (
|
||||
<DlgDeleteCst
|
||||
schema={model.schema}
|
||||
|
|
|
@ -13,9 +13,11 @@ import {
|
|||
BiUpload
|
||||
} from 'react-icons/bi';
|
||||
import { FiEdit } from 'react-icons/fi';
|
||||
import { LuCrown, LuGlasses } from 'react-icons/lu';
|
||||
import { LuCrown, LuGlasses, LuReplace } from 'react-icons/lu';
|
||||
import { VscLibrary } from 'react-icons/vsc';
|
||||
|
||||
import Button from '@/components/ui/Button';
|
||||
import Divider from '@/components/ui/Divider';
|
||||
import Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
|
@ -79,6 +81,11 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
controller.reindex();
|
||||
}
|
||||
|
||||
function handleSubstituteCst() {
|
||||
editMenu.hide();
|
||||
controller.substitute();
|
||||
}
|
||||
|
||||
function handleTemplates() {
|
||||
editMenu.hide();
|
||||
controller.promptTemplate();
|
||||
|
@ -142,11 +149,19 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
icon={<BiTrash size='1rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<DropdownButton
|
||||
text='Создать новую схему'
|
||||
icon={<BiPlusCircle size='1rem' className='clr-text-url' />}
|
||||
onClick={handleCreateNew}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Библиотека'
|
||||
icon={<VscLibrary size='1rem' className='clr-text-url' />}
|
||||
onClick={() => router.push('/library')}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
|
@ -177,6 +192,13 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
icon={<BiDiamond size='1rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||
onClick={handleTemplates}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!controller.isMutable}
|
||||
text='Отождествление'
|
||||
title='Заменить вхождения одной конституенты на другую'
|
||||
icon={<LuReplace size='1rem' className={controller.isMutable ? 'clr-text-primary' : ''} />}
|
||||
onClick={handleSubstituteCst}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
ICstCreatedResponse,
|
||||
ICstMovetoData,
|
||||
ICstRenameData,
|
||||
ICstSubstituteData,
|
||||
ICstUpdateData,
|
||||
IRSFormCreateData,
|
||||
IRSFormData,
|
||||
|
@ -303,6 +304,14 @@ export function patchRenameConstituenta(schema: string, request: FrontExchange<I
|
|||
});
|
||||
}
|
||||
|
||||
export function patchSubstituteConstituenta(schema: string, request: FrontExchange<ICstSubstituteData, IRSFormData>) {
|
||||
AxiosPatch({
|
||||
title: `Substitution for constituenta id=${request.data.original} for schema id=${schema}`,
|
||||
endpoint: `/api/rsforms/${schema}/cst-substitute`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function patchMoveConstituenta(schema: string, request: FrontExchange<ICstMovetoData, IRSFormData>) {
|
||||
AxiosPatch({
|
||||
title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request.data.items)} to ${
|
||||
|
|
Loading…
Reference in New Issue
Block a user