From 95dc3d4b9b9a44420a4b23d32bec3180dca9d403 Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Fri, 1 Mar 2024 18:19:33 +0300 Subject: [PATCH] Implement constituenta substitution and small UI fixes --- .../src/components/ConstituentaSelector.tsx | 48 +++++++++++++ .../frontend/src/components/ui/Tooltip.tsx | 4 +- .../frontend/src/context/RSFormContext.tsx | 22 ++++++ .../frontend/src/dialogs/DlgSubstituteCst.tsx | 70 +++++++++++++++++++ rsconcept/frontend/src/models/rsform.ts | 16 ++++- .../src/pages/RSFormPage/RSEditContext.tsx | 28 ++++++-- .../src/pages/RSFormPage/RSTabsMenu.tsx | 24 ++++++- rsconcept/frontend/src/utils/backendAPI.ts | 9 +++ 8 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 rsconcept/frontend/src/components/ConstituentaSelector.tsx create mode 100644 rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx diff --git a/rsconcept/frontend/src/components/ConstituentaSelector.tsx b/rsconcept/frontend/src/components/ConstituentaSelector.tsx new file mode 100644 index 00000000..90fe5f25 --- /dev/null +++ b/rsconcept/frontend/src/components/ConstituentaSelector.tsx @@ -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 ( + 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; diff --git a/rsconcept/frontend/src/components/ui/Tooltip.tsx b/rsconcept/frontend/src/components/ui/Tooltip.tsx index 1fb5a93d..4e656949 100644 --- a/rsconcept/frontend/src/components/ui/Tooltip.tsx +++ b/rsconcept/frontend/src/components/ui/Tooltip.tsx @@ -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} diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index 0ee10031..9745213c 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -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) => void; cstRename: (data: ICstRenameData, callback?: DataCallback) => void; + cstSubstitute: (data: ICstSubstituteData, callback?: () => void) => void; cstUpdate: (data: ICstUpdateData, callback?: DataCallback) => 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 }} diff --git a/rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx b/rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx new file mode 100644 index 00000000..ad6e946f --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx @@ -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 { + onSubstitute: (data: ICstSubstituteData) => void; +} + +function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) { + const { schema } = useRSForm(); + + const [original, setOriginal] = useState(undefined); + const [substitution, setSubstitution] = useState(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 ( + + + +
+ +
+ + + +
+ ); +} + +export default DlgSubstituteCst; diff --git a/rsconcept/frontend/src/models/rsform.ts b/rsconcept/frontend/src/models/rsform.ts index 8247b2d1..33c00ecb 100644 --- a/rsconcept/frontend/src/models/rsform.ts +++ b/rsconcept/frontend/src/models/rsform.ts @@ -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 {} +/** + * Represents data, used in merging {@link IConstituenta}. + */ +export interface ICstSubstituteData { + original: EntityID; + substitution: EntityID; + transfer_term: boolean; +} + /** * Represents data response when creating {@link IConstituenta}. */ diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx index 8d14c181..01c56cfb 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx @@ -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(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(); const [showCreateCst, setShowCreateCst] = useState(false); @@ -109,8 +113,6 @@ export const RSEditState = ({ const [renameInitialData, setRenameInitialData] = useState(); const [showRenameCst, setShowRenameCst] = useState(false); - const [showEditTerm, setShowEditTerm] = useState(false); - const [insertCstID, setInsertCstID] = useState(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 ? ( + setShowSubstitute(false)} // prettier: split lines + onSubstitute={handleSubstituteCst} + /> + ) : null} {showDeleteCst ? ( } onClick={handleDelete} /> + + + } onClick={handleCreateNew} /> + } + onClick={() => router.push('/library')} + /> @@ -177,6 +192,13 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) { icon={} onClick={handleTemplates} /> + } + onClick={handleSubstituteCst} + /> diff --git a/rsconcept/frontend/src/utils/backendAPI.ts b/rsconcept/frontend/src/utils/backendAPI.ts index a33c3dec..61119bca 100644 --- a/rsconcept/frontend/src/utils/backendAPI.ts +++ b/rsconcept/frontend/src/utils/backendAPI.ts @@ -28,6 +28,7 @@ import { ICstCreatedResponse, ICstMovetoData, ICstRenameData, + ICstSubstituteData, ICstUpdateData, IRSFormCreateData, IRSFormData, @@ -303,6 +304,14 @@ export function patchRenameConstituenta(schema: string, request: FrontExchange) { + 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) { AxiosPatch({ title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request.data.items)} to ${