Improve RSForm and Constituenta UI

includeing RSForm list filtering
This commit is contained in:
IRBorisov 2023-07-16 20:25:55 +03:00
parent db547b9f61
commit 555784b0b1
18 changed files with 252 additions and 47 deletions

View File

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

View File

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

View File

@ -1,6 +1,6 @@
interface SubmitButtonProps {
text: string
loading: boolean
loading?: boolean
disabled?: boolean
}

View File

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

View File

@ -10,7 +10,7 @@ function UserTools() {
};
const navigateMyWork = () => {
navigate('/rsforms?filter=owned');
navigate('/rsforms?filter=personal');
};
return (

View File

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

View File

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

View File

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

View File

@ -68,7 +68,7 @@ export enum ParsingStatus {
// Constituenta data
export interface IConstituenta {
entityUID: number
alias: boolean
alias: string
cstType: CstType
convention?: string
term?: {
@ -140,3 +140,16 @@ export function GetErrLabel(cst: IConstituenta) {
}
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 'Теорема';
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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'
>
@ -60,4 +85,4 @@ function RSFormEditor() {
</div>);
}
export default RSFormEditor;
export default RSFormTabs;

View File

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

View File

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