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} delayShow={1000}
delayHide={100} delayHide={100}
opacity={0.97} 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} classNameArrow={layer}
style={{ ...{ paddingTop: '2px', paddingBottom: '2px', overflowX: 'auto', overflowY: 'auto' }, ...style }} style={{ ...{ paddingTop: '2px', paddingBottom: '2px' }, ...style }}
variant={darkMode ? 'dark' : 'light'} variant={darkMode ? 'dark' : 'light'}
place={place} place={place}
{...restProps} {...restProps}

View File

@ -12,6 +12,7 @@ import {
ICstCreateData, ICstCreateData,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstSubstituteData,
ICstUpdateData, ICstUpdateData,
IRSForm, IRSForm,
IRSFormUploadData IRSFormUploadData
@ -26,6 +27,7 @@ import {
patchMoveConstituenta, patchMoveConstituenta,
patchRenameConstituenta, patchRenameConstituenta,
patchResetAliases, patchResetAliases,
patchSubstituteConstituenta,
patchUploadTRS, patchUploadTRS,
postClaimLibraryItem, postClaimLibraryItem,
postNewConstituenta, postNewConstituenta,
@ -57,6 +59,7 @@ interface IRSFormContext {
cstCreate: (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => void; cstCreate: (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => void;
cstRename: (data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => void; cstRename: (data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => void;
cstSubstitute: (data: ICstSubstituteData, callback?: () => void) => void;
cstUpdate: (data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => void; cstUpdate: (data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => void;
cstDelete: (data: IConstituentaList, callback?: () => void) => void; cstDelete: (data: IConstituentaList, callback?: () => void) => void;
cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void; cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void;
@ -320,6 +323,24 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
[setError, setSchema, library, schemaID] [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( const cstMoveTo = useCallback(
(data: ICstMovetoData, callback?: () => void) => { (data: ICstMovetoData, callback?: () => void) => {
setError(undefined); setError(undefined);
@ -358,6 +379,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
cstUpdate, cstUpdate,
cstCreate, cstCreate,
cstRename, cstRename,
cstSubstitute,
cstDelete, cstDelete,
cstMoveTo 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 // CstType constant for category dividers in TemplateSchemas. TODO: create separate structure for templates
export const CATEGORY_CST_TYPE = CstType.THEOREM; export const CATEGORY_CST_TYPE = CstType.THEOREM;
/**
* Represents Entity identifier type.
*/
export type EntityID = number;
/** /**
* Represents Constituenta classification in terms of system of concepts. * Represents Constituenta classification in terms of system of concepts.
*/ */
@ -58,7 +63,7 @@ export interface TermForm {
* Represents Constituenta basic persistent data. * Represents Constituenta basic persistent data.
*/ */
export interface IConstituentaMeta { export interface IConstituentaMeta {
id: number; id: EntityID;
schema: number; schema: number;
order: number; order: number;
alias: string; alias: string;
@ -130,6 +135,15 @@ export interface ICstUpdateData
*/ */
export interface ICstRenameData extends Pick<IConstituentaMeta, 'id' | 'alias' | 'cst_type'> {} 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}. * Represents data response when creating {@link IConstituenta}.
*/ */

View File

@ -18,6 +18,7 @@ import DlgCreateCst from '@/dialogs/DlgCreateCst';
import DlgDeleteCst from '@/dialogs/DlgDeleteCst'; import DlgDeleteCst from '@/dialogs/DlgDeleteCst';
import DlgEditWordForms from '@/dialogs/DlgEditWordForms'; import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
import DlgRenameCst from '@/dialogs/DlgRenameCst'; import DlgRenameCst from '@/dialogs/DlgRenameCst';
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
import { UserAccessMode } from '@/models/miscellaneous'; import { UserAccessMode } from '@/models/miscellaneous';
import { import {
@ -27,6 +28,7 @@ import {
ICstCreateData, ICstCreateData,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstSubstituteData,
ICstUpdateData, ICstUpdateData,
IRSForm, IRSForm,
TermForm TermForm
@ -54,6 +56,7 @@ interface IRSEditContext {
toggleSubscribe: () => void; toggleSubscribe: () => void;
download: () => void; download: () => void;
reindex: () => void; reindex: () => void;
substitute: () => void;
} }
const RSEditContext = createContext<IRSEditContext | null>(null); const RSEditContext = createContext<IRSEditContext | null>(null);
@ -100,8 +103,9 @@ export const RSEditState = ({
const [showUpload, setShowUpload] = useState(false); const [showUpload, setShowUpload] = useState(false);
const [showClone, setShowClone] = useState(false); const [showClone, setShowClone] = useState(false);
const [showDeleteCst, setShowDeleteCst] = useState(false); const [showDeleteCst, setShowDeleteCst] = useState(false);
const [showEditTerm, setShowEditTerm] = useState(false);
const [showSubstitute, setShowSubstitute] = useState(false);
const [createInitialData, setCreateInitialData] = useState<ICstCreateData>(); const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
const [showCreateCst, setShowCreateCst] = useState(false); const [showCreateCst, setShowCreateCst] = useState(false);
@ -109,8 +113,6 @@ export const RSEditState = ({
const [renameInitialData, setRenameInitialData] = useState<ICstRenameData>(); const [renameInitialData, setRenameInitialData] = useState<ICstRenameData>();
const [showRenameCst, setShowRenameCst] = useState(false); const [showRenameCst, setShowRenameCst] = useState(false);
const [showEditTerm, setShowEditTerm] = useState(false);
const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined); const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined);
const [showTemplates, setShowTemplates] = useState(false); const [showTemplates, setShowTemplates] = useState(false);
@ -150,6 +152,13 @@ export const RSEditState = ({
[model, renameInitialData] [model, renameInitialData]
); );
const handleSubstituteCst = useCallback(
(data: ICstSubstituteData) => {
model.cstSubstitute(data, () => toast.success('Отождествление завершено'));
},
[model]
);
const handleDeleteCst = useCallback( const handleDeleteCst = useCallback(
(deleted: number[]) => { (deleted: number[]) => {
if (!model.schema) { if (!model.schema) {
@ -282,6 +291,10 @@ export const RSEditState = ({
setShowRenameCst(true); setShowRenameCst(true);
}, [activeCst]); }, [activeCst]);
const substitute = useCallback(() => {
setShowSubstitute(true);
}, []);
const editTermForms = useCallback(() => { const editTermForms = useCallback(() => {
if (!activeCst) { if (!activeCst) {
return; return;
@ -370,7 +383,8 @@ export const RSEditState = ({
claim, claim,
share, share,
toggleSubscribe, toggleSubscribe,
reindex reindex,
substitute
}} }}
> >
{model.schema ? ( {model.schema ? (
@ -392,6 +406,12 @@ export const RSEditState = ({
initial={renameInitialData} initial={renameInitialData}
/> />
) : null} ) : null}
{showSubstitute ? (
<DlgSubstituteCst
hideWindow={() => setShowSubstitute(false)} // prettier: split lines
onSubstitute={handleSubstituteCst}
/>
) : null}
{showDeleteCst ? ( {showDeleteCst ? (
<DlgDeleteCst <DlgDeleteCst
schema={model.schema} schema={model.schema}

View File

@ -13,9 +13,11 @@ import {
BiUpload BiUpload
} from 'react-icons/bi'; } from 'react-icons/bi';
import { FiEdit } from 'react-icons/fi'; 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 Button from '@/components/ui/Button';
import Divider from '@/components/ui/Divider';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
@ -79,6 +81,11 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
controller.reindex(); controller.reindex();
} }
function handleSubstituteCst() {
editMenu.hide();
controller.substitute();
}
function handleTemplates() { function handleTemplates() {
editMenu.hide(); editMenu.hide();
controller.promptTemplate(); controller.promptTemplate();
@ -142,11 +149,19 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
icon={<BiTrash size='1rem' className={controller.isMutable ? 'clr-text-warning' : ''} />} icon={<BiTrash size='1rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
onClick={handleDelete} onClick={handleDelete}
/> />
<Divider />
<DropdownButton <DropdownButton
text='Создать новую схему' text='Создать новую схему'
icon={<BiPlusCircle size='1rem' className='clr-text-url' />} icon={<BiPlusCircle size='1rem' className='clr-text-url' />}
onClick={handleCreateNew} onClick={handleCreateNew}
/> />
<DropdownButton
text='Библиотека'
icon={<VscLibrary size='1rem' className='clr-text-url' />}
onClick={() => router.push('/library')}
/>
</Dropdown> </Dropdown>
</div> </div>
@ -177,6 +192,13 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
icon={<BiDiamond size='1rem' className={controller.isMutable ? 'clr-text-success' : ''} />} icon={<BiDiamond size='1rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
onClick={handleTemplates} onClick={handleTemplates}
/> />
<DropdownButton
disabled={!controller.isMutable}
text='Отождествление'
title='Заменить вхождения одной конституенты на другую'
icon={<LuReplace size='1rem' className={controller.isMutable ? 'clr-text-primary' : ''} />}
onClick={handleSubstituteCst}
/>
</Dropdown> </Dropdown>
</div> </div>

View File

@ -28,6 +28,7 @@ import {
ICstCreatedResponse, ICstCreatedResponse,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstSubstituteData,
ICstUpdateData, ICstUpdateData,
IRSFormCreateData, IRSFormCreateData,
IRSFormData, 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>) { export function patchMoveConstituenta(schema: string, request: FrontExchange<ICstMovetoData, IRSFormData>) {
AxiosPatch({ AxiosPatch({
title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request.data.items)} to ${ title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request.data.items)} to ${