mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Improve RSForm and Constituenta UI
includeing RSForm list filtering
This commit is contained in:
parent
db547b9f61
commit
555784b0b1
|
@ -3,6 +3,7 @@ import { config } from './constants'
|
|||
import { ErrorInfo } from './components/BackendError'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ICurrentUser, IRSForm, IUserInfo, IUserProfile } from './models'
|
||||
import { FilterType, RSFormsFilter } from './hooks/useRSForms'
|
||||
|
||||
export type BackendCallback = (response: AxiosResponse) => void;
|
||||
|
||||
|
@ -69,10 +70,17 @@ export async function getActiveUsers(request?: IFrontRequest) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getRSForms(request?: IFrontRequest) {
|
||||
export async function getRSForms(filter: RSFormsFilter, request?: IFrontRequest) {
|
||||
let endpoint: string = ''
|
||||
if (filter.type === FilterType.PERSONAL) {
|
||||
endpoint = `${config.url.BASE}rsforms?owner=${filter.data!}`
|
||||
} else {
|
||||
endpoint = `${config.url.BASE}rsforms?is_common=true`
|
||||
}
|
||||
|
||||
AxiosGet<IRSForm[]>({
|
||||
title: `RSForms list`,
|
||||
endpoint: `${config.url.BASE}rsforms/`,
|
||||
endpoint: endpoint,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ interface CheckboxProps {
|
|||
required?: boolean
|
||||
disabled?: boolean
|
||||
widthClass?: string
|
||||
value?: any
|
||||
value?: boolean
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ function Checkbox({id, required, disabled, label, widthClass='w-full', value, on
|
|||
className='relative cursor-pointer peer w-4 h-4 shrink-0 mt-0.5 bg-white border rounded-sm appearance-none dark:bg-gray-900 checked:bg-blue-700 dark:checked:bg-orange-500'
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Label
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
interface SubmitButtonProps {
|
||||
text: string
|
||||
loading: boolean
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ function TextInput({id, type, required, label, disabled, placeholder, widthClass
|
|||
htmlFor={id}
|
||||
/>
|
||||
<input id={id}
|
||||
className={'px-3 py-2 mt-2 leading-tight border shadow dark:bg-gray-800 '+ widthClass}
|
||||
className={'px-3 py-2 mt-2 leading-tight border shadow dark:bg-gray-800 truncate hover:text-clip '+ widthClass}
|
||||
required={required}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
|
|
|
@ -10,7 +10,7 @@ function UserTools() {
|
|||
};
|
||||
|
||||
const navigateMyWork = () => {
|
||||
navigate('/rsforms?filter=owned');
|
||||
navigate('/rsforms?filter=personal');
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -14,7 +14,7 @@ interface IRSFormContext {
|
|||
isEditable: boolean
|
||||
isClaimable: boolean
|
||||
|
||||
setActive: (cst: IConstituenta) => void
|
||||
setActive: (cst: IConstituenta | undefined) => void
|
||||
reload: () => void
|
||||
upload: (data: any, callback?: BackendCallback) => void
|
||||
destroy: (callback: BackendCallback) => void
|
||||
|
@ -51,12 +51,6 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
|
|||
const isEditable = useMemo(() => (user?.id === schema?.owner || user?.is_staff || false), [user, schema]);
|
||||
const isClaimable = useMemo(() => (user?.id !== schema?.owner || false), [user, schema]);
|
||||
|
||||
useEffect(() => {
|
||||
if (schema?.items && schema?.items.length > 0) {
|
||||
setActive(schema?.items[0]);
|
||||
}
|
||||
}, [schema])
|
||||
|
||||
async function upload(data: any, callback?: BackendCallback) {
|
||||
setError(undefined);
|
||||
patchRSForm(id, {
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { IRSForm } from '../models'
|
||||
import { ErrorInfo } from '../components/BackendError';
|
||||
import { getRSForms } from '../backendAPI';
|
||||
|
||||
export enum FilterType {
|
||||
PERSONAL = 'personal',
|
||||
COMMON = 'common'
|
||||
}
|
||||
|
||||
export interface RSFormsFilter {
|
||||
type: FilterType
|
||||
data?: any
|
||||
}
|
||||
|
||||
export function useRSForms() {
|
||||
const [rsforms, setRSForms] = useState<IRSForm[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorInfo>(undefined);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
getRSForms({
|
||||
const loadList = useCallback(async (filter: RSFormsFilter) => {
|
||||
getRSForms(filter, {
|
||||
showError: true,
|
||||
setLoading: setLoading,
|
||||
onError: error => setError(error),
|
||||
|
@ -17,9 +27,5 @@ export function useRSForms() {
|
|||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData])
|
||||
|
||||
return { rsforms, error, loading };
|
||||
return { rsforms, error, loading, loadList };
|
||||
}
|
|
@ -3,7 +3,6 @@ import { IUserProfile } from '../models'
|
|||
import { ErrorInfo } from '../components/BackendError'
|
||||
import { getProfile } from '../backendAPI'
|
||||
|
||||
|
||||
export function useUserProfile() {
|
||||
const [user, setUser] = useState<IUserProfile | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
|
@ -68,7 +68,7 @@ export enum ParsingStatus {
|
|||
// Constituenta data
|
||||
export interface IConstituenta {
|
||||
entityUID: number
|
||||
alias: boolean
|
||||
alias: string
|
||||
cstType: CstType
|
||||
convention?: string
|
||||
term?: {
|
||||
|
@ -139,4 +139,17 @@ export function GetErrLabel(cst: IConstituenta) {
|
|||
return 'св-во';
|
||||
}
|
||||
return 'ОК';
|
||||
}
|
||||
|
||||
export function GetCstTypeLabel(type: CstType) {
|
||||
switch(type) {
|
||||
case CstType.BASE: return 'Базисное множество';
|
||||
case CstType.CONSTANT: return 'Константное множество';
|
||||
case CstType.STRUCTURED: return 'Родовая структура';
|
||||
case CstType.AXIOM: return 'Аксиома';
|
||||
case CstType.TERM: return 'Терм';
|
||||
case CstType.FUNCTION: return 'Терм-функция';
|
||||
case CstType.PREDICATE: return 'Предикат-функция';
|
||||
case CstType.THEOREM: return 'Теорема';
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ function LoginPage() {
|
|||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!loading) {
|
||||
login(username, password, () => { navigate('/rsforms?filter=owned'); });
|
||||
login(username, password, () => { navigate('/rsforms?filter=personal'); });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -56,7 +56,7 @@ function LoginPage() {
|
|||
|
||||
<div className='flex items-center justify-between mt-4'>
|
||||
<SubmitButton text='Вход' loading={loading}/>
|
||||
<TextURL text='Восстановить пароль...' href='restore-password' />
|
||||
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
||||
|
|
|
@ -80,7 +80,7 @@ function RSFormCreatePage() {
|
|||
/>
|
||||
<Checkbox id='common' label='Общедоступная схема'
|
||||
value={common}
|
||||
onChange={event => setCommon(event.target.value === 'true')}
|
||||
onChange={event => setCommon(event.target.checked)}
|
||||
/>
|
||||
<FileInput id='trs' label='Загрузить *.trs'
|
||||
acceptType='.trs'
|
||||
|
|
|
@ -1,14 +1,122 @@
|
|||
import Card from '../../components/Common/Card';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import PrettyJson from '../../components/Common/PrettyJSON';
|
||||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import { GetCstTypeLabel } from '../../models';
|
||||
import { toast } from 'react-toastify';
|
||||
import TextArea from '../../components/Common/TextArea';
|
||||
import ExpressionEditor from './ExpressionEditor';
|
||||
import SubmitButton from '../../components/Common/SubmitButton';
|
||||
|
||||
function ConstituentEditor() {
|
||||
const { active } = useRSForm();
|
||||
const { active, schema, setActive, isEditable } = useRSForm();
|
||||
|
||||
const [alias, setAlias] = useState('');
|
||||
const [type, setType] = useState('');
|
||||
const [term, setTerm] = useState('');
|
||||
const [textDefinition, setTextDefinition] = useState('');
|
||||
const [expression, setExpression] = useState('');
|
||||
const [convention, setConvention] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!active && schema?.items && schema?.items.length > 0) {
|
||||
setActive(schema?.items[0]);
|
||||
}
|
||||
}, [schema, setActive, active])
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
setAlias(active.alias);
|
||||
setType(GetCstTypeLabel(active.cstType));
|
||||
setTerm(active.term?.raw || '');
|
||||
setTextDefinition(active.definition?.text?.raw || '');
|
||||
setExpression(active.definition?.formal || '');
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
// if (!processing) {
|
||||
// const data = {
|
||||
// 'title': title,
|
||||
// 'alias': alias,
|
||||
// 'comment': comment,
|
||||
// 'is_common': common,
|
||||
// };
|
||||
// upload(data, () => {
|
||||
// toast.success('Изменения сохранены');
|
||||
// reload();
|
||||
// });
|
||||
// }
|
||||
};
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
toast.info('Переименование в разработке');
|
||||
}, []);
|
||||
|
||||
const handleChangeType = useCallback(() => {
|
||||
toast.info('Изменение типа в разработке');
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{active && <PrettyJson data={active}/>}
|
||||
</Card>
|
||||
<div className='flex items-start w-full gap-2'>
|
||||
<form onSubmit={handleSubmit} className='flex-grow min-w-[50rem] px-4 py-2 border'>
|
||||
<div className='flex items-center justify-between gap-1'>
|
||||
<span className='mr-12'>
|
||||
<label
|
||||
title='Переименовать конституенту'
|
||||
className='font-semibold underline cursor-pointer'
|
||||
onClick={handleRename}
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<b className='ml-2'>{alias}</b>
|
||||
</span>
|
||||
<span>
|
||||
<label
|
||||
title='Изменить тип конституенты'
|
||||
className='font-semibold underline cursor-pointer'
|
||||
onClick={handleChangeType}
|
||||
>
|
||||
Тип
|
||||
</label>
|
||||
<span className='ml-2'>{type}</span>
|
||||
</span>
|
||||
</div>
|
||||
<TextArea id='term' label='Термин'
|
||||
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
|
||||
rows={2}
|
||||
value={term}
|
||||
disabled={!isEditable}
|
||||
onChange={event => setTerm(event.target.value)}
|
||||
/>
|
||||
<ExpressionEditor id='expression' label='Формальное выражение'
|
||||
placeholder='Родоструктурное выражение, задающее формальное определение'
|
||||
value={expression}
|
||||
disabled={!isEditable}
|
||||
onChange={event => setExpression(event.target.value)}
|
||||
/>
|
||||
<TextArea id='definition' label='Текстовое определение'
|
||||
placeholder='Лингвистическая интерпретация формального выражения'
|
||||
rows={4}
|
||||
value={textDefinition}
|
||||
disabled={!isEditable}
|
||||
onChange={event => setTextDefinition(event.target.value)}
|
||||
/>
|
||||
<TextArea id='convention' label='Конвенция / Комментарий'
|
||||
placeholder='Договоренность об интерпретации неопределяемых понятий или комментарий к производному понятию'
|
||||
rows={4}
|
||||
value={convention}
|
||||
disabled={!isEditable}
|
||||
onChange={event => setConvention(event.target.value)}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-between gap-1 py-2 mt-2'>
|
||||
<SubmitButton text='Сохранить изменения' disabled={!isEditable} />
|
||||
</div>
|
||||
</form>
|
||||
<PrettyJson data={active || ''} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
35
rsconcept/frontend/src/pages/RSFormPage/ExpressionEditor.tsx
Normal file
35
rsconcept/frontend/src/pages/RSFormPage/ExpressionEditor.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import Label from '../../components/Common/Label';
|
||||
import { useRSForm } from '../../context/RSFormContext';
|
||||
|
||||
interface ExpressionEditorProps {
|
||||
id: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
value: any
|
||||
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
}
|
||||
|
||||
function ExpressionEditor({id, label, disabled, placeholder, value, onChange}: ExpressionEditorProps) {
|
||||
const { schema } = useRSForm();
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3 w-full'>
|
||||
<Label
|
||||
text={label}
|
||||
required={false}
|
||||
htmlFor={id}
|
||||
/>
|
||||
<textarea id='comment'
|
||||
className='w-full px-3 py-2 mt-2 leading-tight border shadow dark:bg-gray-800'
|
||||
rows={6}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExpressionEditor;
|
|
@ -49,7 +49,7 @@ function RSFormCard() {
|
|||
if (window.confirm('Вы уверены, что хотите удалить данную схему?')) {
|
||||
destroy(() => {
|
||||
toast.success('Схема удалена');
|
||||
navigate('/rsforms?filter=owned');
|
||||
navigate('/rsforms?filter=personal');
|
||||
});
|
||||
}
|
||||
}, [destroy, navigate]);
|
||||
|
@ -91,7 +91,7 @@ function RSFormCard() {
|
|||
<Checkbox id='common' label='Общедоступная схема'
|
||||
value={common}
|
||||
disabled={!isEditable}
|
||||
onChange={event => setCommon(event.target.value === 'true')}
|
||||
onChange={event => setCommon(event.target.checked)}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-between gap-1 py-2 mt-2'>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import Card from '../../components/Common/Card';
|
||||
import PrettyJson from '../../components/Common/PrettyJSON';
|
||||
|
||||
function RSFormStats() {
|
||||
const { schema } = useRSForm();
|
||||
|
@ -10,6 +11,7 @@ function RSFormStats() {
|
|||
<label className='font-semibold'>Всего конституент:</label>
|
||||
<span className='ml-2'>{schema!.items!.length}</span>
|
||||
</div>
|
||||
<PrettyJson data={schema || ''}/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,30 +2,55 @@ import { Tabs, TabList, TabPanel } from 'react-tabs';
|
|||
import ConstituentsTable from './ConstituentsTable';
|
||||
import { IConstituenta } from '../../models';
|
||||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import ConceptTab from '../../components/Common/ConceptTab';
|
||||
import RSFormCard from './RSFormCard';
|
||||
import { Loader } from '../../components/Common/Loader';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import ConstituentEditor from './ConstituentEditor';
|
||||
import RSFormStats from './RSFormStats';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
enum RSFormTabs {
|
||||
enum TabsList {
|
||||
CARD = 0,
|
||||
CST_LIST = 1,
|
||||
CST_EDIT = 2
|
||||
}
|
||||
|
||||
function RSFormEditor() {
|
||||
const { setActive, error, schema, loading } = useRSForm();
|
||||
const [tabIndex, setTabIndex] = useState(RSFormTabs.CARD);
|
||||
function RSFormTabs() {
|
||||
const { setActive, active, error, schema, loading } = useRSForm();
|
||||
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', TabsList.CARD);
|
||||
const search = useLocation().search;
|
||||
|
||||
const onEditCst = (cst: IConstituenta) => {
|
||||
console.log(`Set active cst: ${cst.alias}`);
|
||||
setActive(cst);
|
||||
setTabIndex(RSFormTabs.CST_EDIT)
|
||||
setTabIndex(TabsList.CST_EDIT)
|
||||
};
|
||||
|
||||
const onSelectTab = (index: number) => {
|
||||
setTabIndex(index);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const tabQuery = new URLSearchParams(search).get('tab');
|
||||
const activeQuery = new URLSearchParams(search).get('active');
|
||||
const activeCst = schema?.items?.find((cst) => cst.entityUID === Number(activeQuery)) || undefined;
|
||||
setTabIndex(Number(tabQuery) || TabsList.CARD);
|
||||
setActive(activeCst);
|
||||
}, [search, setTabIndex, setActive, schema?.items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (schema) {
|
||||
let url = `/rsforms/${schema.id}?tab=${tabIndex}`
|
||||
if (active) {
|
||||
url = url + `&active=${active.entityUID}`
|
||||
}
|
||||
window.history.replaceState(null, '', url);
|
||||
}
|
||||
}, [tabIndex, active, schema]);
|
||||
|
||||
return (
|
||||
<div className='container w-full'>
|
||||
{ loading && <Loader /> }
|
||||
|
@ -33,7 +58,7 @@ function RSFormEditor() {
|
|||
{ schema && !loading &&
|
||||
<Tabs
|
||||
selectedIndex={tabIndex}
|
||||
onSelect={(index) => setTabIndex(index)}
|
||||
onSelect={onSelectTab}
|
||||
defaultFocus={true}
|
||||
selectedTabClassName='font-bold'
|
||||
>
|
||||
|
@ -43,7 +68,7 @@ function RSFormEditor() {
|
|||
<ConceptTab>Редактор</ConceptTab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel className='flex items-start w-full gap-2 '>
|
||||
<TabPanel className='flex items-start w-full gap-2'>
|
||||
<RSFormCard />
|
||||
<RSFormStats />
|
||||
</TabPanel>
|
||||
|
@ -60,4 +85,4 @@ function RSFormEditor() {
|
|||
</div>);
|
||||
}
|
||||
|
||||
export default RSFormEditor;
|
||||
export default RSFormTabs;
|
|
@ -1,12 +1,12 @@
|
|||
import { useParams } from 'react-router-dom';
|
||||
import { RSFormState } from '../../context/RSFormContext';
|
||||
import RSFormEditor from './RSFormEditor';
|
||||
import RSFormTabs from './RSFormTabs';
|
||||
|
||||
function RSFormPage() {
|
||||
const { id } = useParams();
|
||||
return (
|
||||
<RSFormState id={id || ''}>
|
||||
<RSFormEditor />
|
||||
<RSFormTabs />
|
||||
</RSFormState>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
import { useLocation } from 'react-router-dom';
|
||||
import BackendError from '../../components/BackendError'
|
||||
import { Loader } from '../../components/Common/Loader'
|
||||
import { useRSForms } from '../../hooks/useRSForms'
|
||||
import { FilterType, RSFormsFilter, useRSForms } from '../../hooks/useRSForms'
|
||||
import RSFormsTable from './RSFormsTable';
|
||||
import { useEffect } from 'react';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
|
||||
function RSFormsPage() {
|
||||
const { rsforms, error, loading } = useRSForms();
|
||||
const search = useLocation().search;
|
||||
const { user } = useAuth();
|
||||
const { rsforms, error, loading, loadList } = useRSForms();
|
||||
|
||||
useEffect(() => {
|
||||
const filterQuery = new URLSearchParams(search).get('filter');
|
||||
const type = (!user || !filterQuery ? FilterType.COMMON : filterQuery as FilterType);
|
||||
let filter: RSFormsFilter = {type: type};
|
||||
if (type === FilterType.PERSONAL) {
|
||||
filter.data = user?.id;
|
||||
}
|
||||
loadList(filter);
|
||||
}, [search, user, loadList]);
|
||||
|
||||
return (
|
||||
<div className='container'>
|
||||
|
|
Loading…
Reference in New Issue
Block a user