Implement constituenta substitution and small UI fixes

This commit is contained in:
IRBorisov 2024-03-01 18:19:33 +03:00
parent 52ed20942e
commit 95dc3d4b9b
8 changed files with 213 additions and 8 deletions

View 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;

View File

@ -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}

View File

@ -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
}}

View 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;

View File

@ -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}.
*/

View File

@ -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}

View File

@ -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>

View File

@ -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 ${