Improve UI procedures

This commit is contained in:
IRBorisov 2023-07-21 00:09:05 +03:00
parent ff564704fe
commit 94961deb8a
15 changed files with 172 additions and 102 deletions

View File

@ -7,7 +7,7 @@ interface DropdownProps {
function Dropdown({children, widthClass='w-fit', stretchLeft}: DropdownProps) {
return (
<div className='relative'>
<div className={`absolute ${stretchLeft ? 'right-0': 'left-0'} z-10 flex flex-col items-stretch justify-start p-2 mt-2 text-sm origin-top-right bg-white border border-gray-100 divide-y rounded-md shadow-lg dark:border-gray-500 dark:bg-gray-900 ${widthClass}`}>
<div className={`absolute ${stretchLeft ? 'right-0': 'left-0'} py-2 z-10 flex flex-col items-stretch justify-start px-2 mt-2 text-sm origin-top-right bg-white border border-gray-100 divide-y rounded-md shadow-lg dark:border-gray-500 dark:bg-gray-900 ${widthClass}`}>
{children}
</div>
</div>

View File

@ -0,0 +1,22 @@
interface NavigationTextItemProps {
description?: string | undefined
onClick?: () => void
disabled?: boolean
children: React.ReactNode
}
function DropdownButton({description='', onClick, disabled, children}: NavigationTextItemProps) {
const behavior = (onClick ? 'cursor-pointer clr-hover': 'cursor-default') + ' disabled:cursor-not-allowed';
return (
<button
disabled={disabled}
title={description}
onClick={onClick}
className={`px-3 py-1 text-left overflow-ellipsis ${behavior} whitespace-nowrap`}
>
{children}
</button>
);
}
export default DropdownButton;

View File

@ -7,7 +7,7 @@ interface LabeledTextProps {
function LabeledText({id, label, text, tooltip}: LabeledTextProps) {
return (
<div className='flex justify-between gap-2'>
<div className='flex justify-between gap-4'>
<label
className='font-semibold'
title={tooltip}

View File

@ -63,7 +63,7 @@ export function EyeOffIcon({size}: IconProps) {
export function PenIcon({size}: IconProps) {
return (
<IconSVG viewbox='0 0 16 16' size={size}>
<IconSVG viewbox='-3 -3 21 21' size={size}>
<path d='M15.502 1.94a.5.5 0 010 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 01.707 0l1.293 1.293zm-1.75 2.456l-2-2L4.939 9.21a.5.5 0 00-.121.196l-.805 2.414a.25.25 0 00.316.316l2.414-.805a.5.5 0 00.196-.12l6.813-6.814z' />
<path d='M1 13.5A1.5 1.5 0 002.5 15h11a1.5 1.5 0 001.5-1.5v-6a.5.5 0 00-1 0v6a.5.5 0 01-.5.5h-11a.5.5 0 01-.5-.5v-11a.5.5 0 01.5-.5H9a.5.5 0 000-1H2.5A1.5 1.5 0 001 2.5v11z' />
</IconSVG>

View File

@ -1,19 +0,0 @@
interface NavigationTextItemProps {
text?: string | undefined
description?: string | undefined
onClick: () => void
bold?: boolean
}
function NavigationTextItem({text='', description='', onClick, bold}: NavigationTextItemProps) {
return (
<button
title={description}
onClick={onClick}
className={(bold ? 'font-bold ': '') + 'px-4 py-1 hover:bg-gray-50 hover:text-gray-700 dark:hover:text-white dark:hover:bg-gray-500 overflow-ellipsis text-left'}>
{text}
</button>
);
}
export default NavigationTextItem;

View File

@ -1,6 +1,6 @@
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import NavigationTextItem from './NavigationTextItem';
import DropdownButton from '../Common/DropdownButton';
import { useTheme } from '../../context/ThemeContext';
import Dropdown from '../Common/Dropdown';
@ -29,17 +29,19 @@ function UserDropdown({hideDropdown}: UserDropdownProps) {
};
return (
<Dropdown widthClass='w-36' stretchLeft >
<NavigationTextItem description='Профиль пользователя'
text={user?.username}
onClick={navigateProfile}
/>
<NavigationTextItem description='Переключение темы оформления'
text={darkMode ? 'Светлая тема' : 'Темная тема'}
onClick={toggleDarkMode}
/>
<NavigationTextItem text={'Мои схемы'} onClick={navigateMyWork} />
<NavigationTextItem text={'Выйти...'} bold onClick={logoutAndRedirect} />
<Dropdown widthClass='w-36' stretchLeft>
<DropdownButton description='Профиль пользователя' onClick={navigateProfile}>
{user?.username}
</DropdownButton>
<DropdownButton description='Переключение темы оформления' onClick={toggleDarkMode}>
{darkMode ? 'Светлая тема' : 'Темная тема'}
</DropdownButton>
<DropdownButton onClick={navigateMyWork}>
Мои схемы
</DropdownButton>
<DropdownButton onClick={logoutAndRedirect}>
<b>Выйти...</b>
</DropdownButton>
</Dropdown>
);
}

View File

@ -12,15 +12,16 @@ interface IRSFormContext {
error: ErrorInfo
loading: boolean
processing: boolean
isOwned: boolean
isEditable: boolean
isClaimable: boolean
forceAdmin: boolean
readonly: boolean
isTracking: boolean
setActive: (cst: IConstituenta | undefined) => void
setForceAdmin: (value: boolean) => void
setReadonly: (value: boolean) => void
setActive: React.Dispatch<React.SetStateAction<IConstituenta | undefined>>
toggleForceAdmin: () => void
toggleReadonly: () => void
toggleTracking: () => void
reload: () => void
update: (data: any, callback?: BackendCallback) => void
@ -37,6 +38,7 @@ export const RSFormContext = createContext<IRSFormContext>({
error: undefined,
loading: false,
processing: false,
isOwned: false,
isEditable: false,
isClaimable: false,
forceAdmin: false,
@ -44,8 +46,8 @@ export const RSFormContext = createContext<IRSFormContext>({
isTracking: true,
setActive: () => {},
setForceAdmin: () => {},
setReadonly: () => {},
toggleForceAdmin: () => {},
toggleReadonly: () => {},
toggleTracking: () => {},
reload: () => {},
update: () => {},
@ -70,23 +72,23 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
const [forceAdmin, setForceAdmin] = useState(false);
const [readonly, setReadonly] = useState(false);
const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema]);
const isClaimable = useMemo(() => (user?.id !== schema?.owner || false), [user, schema]);
const isEditable = useMemo(() => {
return (
!readonly &&
(user?.id === schema?.owner || (forceAdmin && user?.is_staff) || false)
(isOwned || (forceAdmin && user?.is_staff) || false)
)
}, [user, schema, readonly, forceAdmin]);
}, [user, readonly, forceAdmin, isOwned]);
const isTracking = useMemo(() => {
return true;
}, []);
const toggleTracking = useCallback(() => {
toast('not implemented yet');
}, []);
const isClaimable = useMemo(() => (user?.id !== schema?.owner || false), [user, schema]);
async function update(data: any, callback?: BackendCallback) {
setError(undefined);
patchRSForm(id, {
@ -143,9 +145,10 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
<RSFormContext.Provider value={{
schema, error, loading, processing,
active, setActive,
forceAdmin, setForceAdmin,
readonly, setReadonly,
isEditable, isClaimable,
forceAdmin, readonly,
toggleForceAdmin: () => setForceAdmin(prev => !prev),
toggleReadonly: () => setReadonly(prev => !prev),
isOwned, isEditable, isClaimable,
isTracking, toggleTracking,
cstUpdate,
reload, update, download, destroy, claim

View File

@ -23,6 +23,10 @@
@apply text-zinc-200 bg-gray-900
}
.clr-hover {
@apply hover:bg-gray-50 hover:text-gray-700 dark:hover:text-white dark:hover:bg-gray-500
}
.text-red {
@apply text-red-400 dark:text-red-600
}

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { useRSForm } from '../../context/RSFormContext';
import { EditMode } from '../../utils/models';
import { EditMode, IConstituenta } from '../../utils/models';
import { toast } from 'react-toastify';
import TextArea from '../../components/Common/TextArea';
import ExpressionEditor from './ExpressionEditor';
@ -24,10 +24,10 @@ function ConstituentEditor() {
const [typification, setTypification] = useState('N/A');
useEffect(() => {
if (!active && schema?.items && schema?.items.length > 0) {
setActive(schema?.items[0]);
if (schema?.items && schema?.items.length > 0) {
setActive((prev) => (prev || schema?.items![0]));
}
}, [schema, setActive, active])
}, [schema, setActive])
useEffect(() => {
if (active) {

View File

@ -32,13 +32,12 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) {
const handleRowClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
if (event.ctrlKey) {
console.log('ctrl + click');
setActive(cst);
}
}, []);
}, [setActive]);
const handleDoubleClick = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
console.log('activating')
setActive(cst);
}, [setActive]);

View File

@ -19,7 +19,6 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
const handleRowSelected = useCallback(
({selectedRows} : SelectionInfo<IConstituenta>) => {
console.log('on selection change')
setSelectedRows(selectedRows);
}, []);

View File

@ -12,12 +12,15 @@ import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import fileDownload from 'js-file-download';
import { AxiosResponse } from 'axios';
import { useAuth } from '../../context/AuthContext';
import { claimOwnershipProc, deleteRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
function RSFormCard() {
const navigate = useNavigate();
const intl = useIntl();
const { getUserLabel } = useUsers();
const { schema, update, download, reload, isEditable, isClaimable, processing, destroy, claim } = useRSForm();
const { user } = useAuth();
const [title, setTitle] = useState('');
const [alias, setAlias] = useState('');
@ -45,23 +48,8 @@ function RSFormCard() {
});
};
const handleDelete = useCallback(() => {
if (window.confirm('Вы уверены, что хотите удалить данную схему?')) {
destroy(() => {
toast.success('Схема удалена');
navigate('/rsforms?filter=personal');
});
}
}, [destroy, navigate]);
const handleClaimOwner = useCallback(() => {
if (window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
claim(() => {
toast.success('Вы стали владельцем схемы');
reload();
});
}
}, [claim, reload]);
const handleDelete =
useCallback(() => deleteRSFormProc(destroy, navigate), [destroy, navigate]);
const handleDownload = useCallback(() => {
download((response: AxiosResponse) => {
@ -74,12 +62,6 @@ function RSFormCard() {
});
}, [download, schema?.alias]);
const handleShare = useCallback(() => {
const url = window.location.href + '&share';
navigator.clipboard.writeText(url);
toast.success(`Ссылка скопирована: ${url}`);
}, []);
return (
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'>
<TextInput id='title' label='Полное название' type='text'
@ -112,21 +94,23 @@ function RSFormCard() {
<Button
tooltip='Поделиться схемой'
icon={<ShareIcon />}
onClick={handleShare}
colorClass='text-primary'
onClick={shareCurrentURLProc}
/>
<Button
disabled={processing}
tooltip='Скачать TRS файл'
icon={<DownloadIcon />}
colorClass='text-primary'
loading={processing}
onClick={handleDownload}
/>
<Button
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
disabled={!isClaimable || processing}
disabled={!isClaimable || processing || !user}
icon={<CrownIcon />}
colorClass='text-green'
onClick={handleClaimOwner}
onClick={() => claimOwnershipProc(claim, reload)}
/>
<Button
tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'}

View File

@ -2,7 +2,7 @@ import { Tabs, TabList, TabPanel } from 'react-tabs';
import ConstituentsTable from './ConstituentsTable';
import { IConstituenta } from '../../utils/models';
import { useRSForm } from '../../context/RSFormContext';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import ConceptTab from '../../components/Common/ConceptTab';
import RSFormCard from './RSFormCard';
import { Loader } from '../../components/Common/Loader';
@ -21,6 +21,7 @@ enum TabsList {
function RSFormTabs() {
const { setActive, active, error, schema, loading } = useRSForm();
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', TabsList.CARD);
const [init, setInit] = useState(false);
const onEditCst = (cst: IConstituenta) => {
console.log(`Set active cst: ${cst.alias}`);
@ -33,11 +34,14 @@ function RSFormTabs() {
};
useEffect(() => {
const url = new URL(window.location.href);
const activeQuery = url.searchParams.get('active');
const activeCst = schema?.items?.find((cst) => cst.entityUID === Number(activeQuery)) || undefined;
setActive(activeCst);
}, [setActive, schema?.items]);
if (schema) {
const url = new URL(window.location.href);
const activeQuery = url.searchParams.get('active');
const activeCst = schema?.items?.find((cst) => cst.entityUID === Number(activeQuery)) || undefined;
setActive(activeCst);
setInit(true);
}
}, [setActive, schema, setInit]);
useEffect(() => {
const url = new URL(window.location.href);
@ -46,15 +50,17 @@ function RSFormTabs() {
}, [setTabIndex]);
useEffect(() => {
let url = new URL(window.location.href);
url.searchParams.set('tab', String(tabIndex));
if (active) {
url.searchParams.set('active', String(active.entityUID));
} else {
url.searchParams.delete('active');
if (init) {
let url = new URL(window.location.href);
url.searchParams.set('tab', String(tabIndex));
if (active) {
url.searchParams.set('active', String(active.entityUID));
} else {
url.searchParams.delete('active');
}
window.history.pushState(null, '', url.toString());
}
window.history.replaceState(null, '', url.toString());
}, [tabIndex, active]);
}, [tabIndex, active, init]);
return (
<div className='w-full'>

View File

@ -1,14 +1,33 @@
import { useCallback } from 'react';
import Button from '../../components/Common/Button';
import Dropdown from '../../components/Common/Dropdown';
import { EyeIcon, EyeOffIcon, MenuIcon, PenIcon } from '../../components/Icons';
import { CrownIcon, DumpBinIcon, EyeIcon, EyeOffIcon, MenuIcon, PenIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import useDropdown from '../../hooks/useDropdown';
import DropdownButton from '../../components/Common/DropdownButton';
import Checkbox from '../../components/Common/Checkbox';
import { useAuth } from '../../context/AuthContext';
import { claimOwnershipProc, deleteRSFormProc } from '../../utils/procedures';
import { useNavigate } from 'react-router-dom';
function TablistTools() {
const { isEditable, isTracking, toggleTracking } = useRSForm();
const navigate = useNavigate();
const {user} = useAuth();
const {
isOwned, isEditable, isTracking, readonly, forceAdmin,
toggleTracking, toggleForceAdmin, toggleReadonly,
claim, reload, destroy
} = useRSForm();
const schemaMenu = useDropdown();
const editMenu = useDropdown();
const handleClaimOwner = useCallback(() => {
claimOwnershipProc(claim, reload)
}, [claim, reload]);
const handleDelete =
useCallback(() => deleteRSFormProc(destroy, navigate), [destroy, navigate]);
return (
<div className='flex items-center w-fit'>
<div ref={schemaMenu.ref}>
@ -21,10 +40,14 @@ function TablistTools() {
/>
{ schemaMenu.isActive &&
<Dropdown>
<p className='whitespace-nowrap'>стать владельцем</p>
<p>клонировать</p>
<p>поделиться</p>
<p>удалить</p>
<DropdownButton disabled={isEditable} onClick={handleDelete}>
<div className='inline-flex items-center gap-1 justify-normal'>
<span className={isOwned ? 'text-red' : ''}><DumpBinIcon size={4} /></span>
<p>Удалить схему</p>
</div>
</DropdownButton>
</Dropdown>}
</div>
<div ref={editMenu.ref}>
@ -38,9 +61,22 @@ function TablistTools() {
/>
{ editMenu.isActive &&
<Dropdown>
<p className='whitespace-nowrap'>стать владельцем / уже владелец</p>
<p>ридонли</p>
<p>админ оверрайд</p>
<DropdownButton disabled={!user} onClick={!isOwned ? handleClaimOwner : undefined}>
<div className='inline-flex items-center gap-1 justify-normal'>
<span className={isOwned ? 'text-green' : ''}><CrownIcon size={4} /></span>
<p>
{ isOwned && <b>Владелец схемы</b> }
{ !isOwned && <b>Стать владельцем</b> }
</p>
</div>
</DropdownButton>
<DropdownButton onClick={toggleReadonly}>
<Checkbox value={readonly} label='только чтение'/>
</DropdownButton>
{user?.is_staff &&
<DropdownButton onClick={toggleForceAdmin}>
<Checkbox value={forceAdmin} label='режим администратора'/>
</DropdownButton>}
</Dropdown>}
</div>
<div>

View File

@ -0,0 +1,34 @@
import { toast } from 'react-toastify';
import { BackendCallback } from './backendAPI';
export function shareCurrentURLProc() {
const url = window.location.href + '&share';
navigator.clipboard.writeText(url);
toast.success(`Ссылка скопирована: ${url}`);
}
export function claimOwnershipProc(
claim: (callback: BackendCallback) => void,
reload: Function
) {
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
return;
}
claim(() => {
toast.success('Вы стали владельцем схемы');
reload();
});
}
export function deleteRSFormProc(
destroy: (callback: BackendCallback) => void,
navigate: Function
) {
if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) {
return;
}
destroy(() => {
toast.success('Схема удалена');
navigate('/rsforms?filter=personal');
});
}