mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-14 12:50:37 +03:00
Refactor SelectTree and minor UI fixes
This commit is contained in:
parent
eee96aecd1
commit
cfd886e107
|
@ -5,7 +5,7 @@ import Button from '../components/ui/Button';
|
|||
|
||||
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
<div className='flex flex-col items-center antialiased clr-app' role='alert'>
|
||||
<div className='flex flex-col gap-3 items-center antialiased clr-app' role='alert'>
|
||||
<h1>Что-то пошло не так!</h1>
|
||||
<Button onClick={resetErrorBoundary} text='Попробовать еще раз' />
|
||||
<InfoError error={error as Error} />
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import axios, { type AxiosError } from 'axios';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { external_urls } from '@/utils/constants';
|
||||
import { isResponseHtml } from '@/utils/utils';
|
||||
|
||||
import PrettyJson from '../ui/PrettyJSON';
|
||||
import TextURL from '../ui/TextURL';
|
||||
import AnimateFade from '../wrap/AnimateFade';
|
||||
|
||||
export type ErrorData = string | Error | AxiosError | undefined;
|
||||
|
@ -70,12 +68,12 @@ function InfoError({ error }: InfoErrorProps) {
|
|||
'select-text'
|
||||
)}
|
||||
>
|
||||
<p className='font-normal clr-text-default'>
|
||||
Пожалуйста сделайте скриншот и отправьте вместе с описанием ситуации на почту{' '}
|
||||
<TextURL href={external_urls.mail_portal} text='portal@acconcept.ru' />
|
||||
<div className='font-normal clr-text-default'>
|
||||
<p>Пожалуйста сделайте скриншот и отправьте вместе с описанием ситуации на почту portal@acconcept.ru</p>
|
||||
<br />
|
||||
Для продолжения работы перезагрузите страницу
|
||||
</p>
|
||||
<p>Для продолжения работы перезагрузите страницу</p>
|
||||
</div>
|
||||
|
||||
<DescribeError error={error} />
|
||||
</AnimateFade>
|
||||
);
|
||||
|
|
112
rsconcept/frontend/src/components/ui/SelectTree.tsx
Normal file
112
rsconcept/frontend/src/components/ui/SelectTree.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import clsx from 'clsx';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { animateSideAppear } from '@/styling/animations';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
import { IconDropArrow, IconPageRight } from '../Icons';
|
||||
import { CProps } from '../props';
|
||||
import MiniButton from './MiniButton';
|
||||
import Overlay from './Overlay';
|
||||
|
||||
interface SelectTreeProps<ItemType> extends CProps.Styling {
|
||||
items: ItemType[];
|
||||
value: ItemType;
|
||||
setValue: (newItem: ItemType) => void;
|
||||
getParent: (item: ItemType) => ItemType;
|
||||
getLabel: (item: ItemType) => string;
|
||||
getDescription: (item: ItemType) => string;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
function SelectTree<ItemType>({
|
||||
items,
|
||||
value,
|
||||
getParent,
|
||||
getLabel,
|
||||
getDescription,
|
||||
setValue,
|
||||
prefix,
|
||||
...restProps
|
||||
}: SelectTreeProps<ItemType>) {
|
||||
const foldable = useMemo(
|
||||
() => new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item))),
|
||||
[items, getParent]
|
||||
);
|
||||
const [folded, setFolded] = useState<ItemType[]>(items);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item));
|
||||
}, [value, getParent, items]);
|
||||
|
||||
const onFoldItem = useCallback(
|
||||
(target: ItemType, showChildren: boolean) => {
|
||||
setFolded(prev =>
|
||||
items.filter(item => {
|
||||
if (item === target) {
|
||||
return !showChildren;
|
||||
}
|
||||
if (!showChildren && (getParent(item) === target || getParent(getParent(item)) === target)) {
|
||||
return true;
|
||||
} else {
|
||||
return prev.includes(item);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
[items, getParent]
|
||||
);
|
||||
|
||||
const handleClickFold = useCallback(
|
||||
(event: CProps.EventMouse, target: ItemType, showChildren: boolean) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onFoldItem(target, showChildren);
|
||||
},
|
||||
[onFoldItem]
|
||||
);
|
||||
|
||||
return (
|
||||
<div {...restProps}>
|
||||
<AnimatePresence initial={false}>
|
||||
{items.map((item, index) =>
|
||||
getParent(item) === item || !folded.includes(getParent(item)) ? (
|
||||
<motion.div
|
||||
tabIndex={-1}
|
||||
key={`${prefix}${index}`}
|
||||
className={clsx(
|
||||
'pr-3 pl-6 py-1',
|
||||
'cc-scroll-row',
|
||||
'clr-controls clr-hover',
|
||||
'cursor-pointer',
|
||||
value === item && 'clr-selected'
|
||||
)}
|
||||
data-tooltip-id={globals.tooltip}
|
||||
data-tooltip-content={getDescription(item)}
|
||||
onClick={() => setValue(item)}
|
||||
initial={{ ...animateSideAppear.initial }}
|
||||
animate={{ ...animateSideAppear.animate }}
|
||||
exit={{ ...animateSideAppear.exit }}
|
||||
>
|
||||
{foldable.has(item) ? (
|
||||
<Overlay position='left-[-1.3rem]' className={clsx(!folded.includes(item) && 'top-[0.1rem]')}>
|
||||
<MiniButton
|
||||
tabIndex={-1}
|
||||
noPadding
|
||||
noHover
|
||||
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
||||
onClick={event => handleClickFold(event, item, folded.includes(item))}
|
||||
/>
|
||||
</Overlay>
|
||||
) : null}
|
||||
{getParent(item) === item ? getLabel(item) : `- ${getLabel(item).toLowerCase()}`}
|
||||
</motion.div>
|
||||
) : null
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectTree;
|
|
@ -32,7 +32,13 @@ function Tooltip({
|
|||
delayShow={1000}
|
||||
delayHide={100}
|
||||
opacity={0.97}
|
||||
className={clsx('overflow-auto sm:overflow-hidden overscroll-contain', 'border shadow-md', layer, className)}
|
||||
className={clsx(
|
||||
'overflow-auto sm:overflow-hidden overscroll-contain',
|
||||
'border shadow-md',
|
||||
'text-balance',
|
||||
layer,
|
||||
className
|
||||
)}
|
||||
classNameArrow={layer}
|
||||
style={{ ...{ paddingTop: '2px', paddingBottom: '2px' }, ...style }}
|
||||
variant={darkMode ? 'dark' : 'light'}
|
||||
|
|
|
@ -6,12 +6,13 @@ import { useCallback } from 'react';
|
|||
|
||||
import { IconMenuFold, IconMenuUnfold } from '@/components/Icons';
|
||||
import Button from '@/components/ui/Button';
|
||||
import SelectTree from '@/components/ui/SelectTree';
|
||||
import { useConceptOptions } from '@/context/OptionsContext';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { HelpTopic, topicParent } from '@/models/miscellaneous';
|
||||
import { animateSlideLeft } from '@/styling/animations';
|
||||
|
||||
import TopicsTree from './TopicsTree';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
|
||||
|
||||
interface TopicsDropdownProps {
|
||||
activeTopic: HelpTopic;
|
||||
|
@ -22,7 +23,7 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
|
|||
const menu = useDropdown();
|
||||
const { noNavigation, calculateHeight } = useConceptOptions();
|
||||
|
||||
const selectTheme = useCallback(
|
||||
const handleSelectTopic = useCallback(
|
||||
(topic: HelpTopic) => {
|
||||
menu.hide();
|
||||
onChangeTopic(topic);
|
||||
|
@ -65,7 +66,15 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
|
|||
animate={menu.isOpen ? 'open' : 'closed'}
|
||||
variants={animateSlideLeft}
|
||||
>
|
||||
<TopicsTree activeTopic={activeTopic} onChangeTopic={selectTheme} />
|
||||
<SelectTree
|
||||
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
||||
value={activeTopic}
|
||||
setValue={handleSelectTopic}
|
||||
prefix={prefixes.topic_list}
|
||||
getParent={item => topicParent.get(item) ?? item}
|
||||
getLabel={labelHelpTopic}
|
||||
getDescription={describeHelpTopic}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import SelectTree from '@/components/ui/SelectTree';
|
||||
import { useConceptOptions } from '@/context/OptionsContext';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
import TopicsTree from './TopicsTree';
|
||||
import { HelpTopic, topicParent } from '@/models/miscellaneous';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
|
||||
|
||||
interface TopicsStaticProps {
|
||||
activeTopic: HelpTopic;
|
||||
|
@ -13,7 +14,14 @@ interface TopicsStaticProps {
|
|||
function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
|
||||
const { calculateHeight } = useConceptOptions();
|
||||
return (
|
||||
<div
|
||||
<SelectTree
|
||||
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
||||
value={activeTopic}
|
||||
setValue={onChangeTopic}
|
||||
prefix={prefixes.topic_list}
|
||||
getParent={item => topicParent.get(item) ?? item}
|
||||
getLabel={labelHelpTopic}
|
||||
getDescription={describeHelpTopic}
|
||||
className={clsx(
|
||||
'sticky top-0 left-0',
|
||||
'w-[14.5rem] cc-scroll-y',
|
||||
|
@ -24,9 +32,7 @@ function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
|
|||
'select-none'
|
||||
)}
|
||||
style={{ maxHeight: calculateHeight('2.25rem + 2px') }}
|
||||
>
|
||||
<TopicsTree activeTopic={activeTopic} onChangeTopic={onChangeTopic} />
|
||||
</div>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { IconDropArrow, IconPageRight } from '@/components/Icons';
|
||||
import { CProps } from '@/components/props';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { foldableTopics, HelpTopic, topicParent } from '@/models/miscellaneous';
|
||||
import { animateSideAppear } from '@/styling/animations';
|
||||
import { globals, prefixes } from '@/utils/constants';
|
||||
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
|
||||
|
||||
interface TopicsTreeProps {
|
||||
activeTopic: HelpTopic;
|
||||
onChangeTopic: (newTopic: HelpTopic) => void;
|
||||
}
|
||||
|
||||
function TopicsTree({ activeTopic, onChangeTopic }: TopicsTreeProps) {
|
||||
const [topicFolded, setFolded] = useState<Map<HelpTopic, boolean>>(
|
||||
new Map(
|
||||
Object.values(HelpTopic).map(value => {
|
||||
const topic = value as HelpTopic;
|
||||
return [topic, true];
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setFolded(
|
||||
new Map(
|
||||
Object.values(HelpTopic).map(value => {
|
||||
const topic = value as HelpTopic;
|
||||
return [
|
||||
topic,
|
||||
topicParent.get(activeTopic) !== topic && topicParent.get(topicParent.get(activeTopic)!) !== topic
|
||||
];
|
||||
})
|
||||
)
|
||||
);
|
||||
}, [activeTopic]);
|
||||
|
||||
const onFoldTopic = useCallback(
|
||||
(target: HelpTopic, showChildren: boolean) => {
|
||||
if (topicFolded.get(target) === !showChildren) {
|
||||
return;
|
||||
}
|
||||
setFolded(
|
||||
new Map(
|
||||
Object.values(HelpTopic).map(value => {
|
||||
const topic = value as HelpTopic;
|
||||
if (topic === target) {
|
||||
return [topic, !showChildren];
|
||||
}
|
||||
if (
|
||||
!showChildren &&
|
||||
(topicParent.get(topic) === target || topicParent.get(topicParent.get(topic)!) === target)
|
||||
) {
|
||||
return [topic, true];
|
||||
}
|
||||
const oldValue = topicFolded.get(topic)!;
|
||||
return [topic, oldValue];
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
[topicFolded]
|
||||
);
|
||||
|
||||
const handleClickFold = useCallback(
|
||||
(event: CProps.EventMouse, topic: HelpTopic, showChildren: boolean) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onFoldTopic(topic, showChildren);
|
||||
},
|
||||
[onFoldTopic]
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={false}>
|
||||
{Object.values(HelpTopic).map((topic, index) => {
|
||||
const parent = topicParent.get(topic);
|
||||
if (parent !== topic && topicFolded.get(topicParent.get(topic)!)) {
|
||||
return null;
|
||||
}
|
||||
const isFoldable = !!foldableTopics.find(id => id === topic);
|
||||
const isFolded = topicFolded.get(topic)!;
|
||||
return (
|
||||
<motion.div
|
||||
tabIndex={-1}
|
||||
key={`${prefixes.topic_list}${index}`}
|
||||
className={clsx(
|
||||
'pr-3 pl-6 py-1',
|
||||
'cc-scroll-row',
|
||||
'clr-controls clr-hover',
|
||||
'cursor-pointer',
|
||||
activeTopic === topic && 'clr-selected'
|
||||
)}
|
||||
data-tooltip-id={globals.tooltip}
|
||||
data-tooltip-content={describeHelpTopic(topic)}
|
||||
onClick={() => onChangeTopic(topic)}
|
||||
initial={{ ...animateSideAppear.initial }}
|
||||
animate={{ ...animateSideAppear.animate }}
|
||||
exit={{ ...animateSideAppear.exit }}
|
||||
>
|
||||
{isFoldable ? (
|
||||
<Overlay position='left-[-1.3rem]' className={clsx(!isFolded && 'top-[0.1rem]')}>
|
||||
<MiniButton
|
||||
tabIndex={-1}
|
||||
noPadding
|
||||
noHover
|
||||
icon={!isFolded ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
||||
onClick={event => handleClickFold(event, topic, isFolded)}
|
||||
/>
|
||||
</Overlay>
|
||||
) : null}
|
||||
{topicParent.get(topic) === topic ? labelHelpTopic(topic) : `- ${labelHelpTopic(topic).toLowerCase()}`}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
export default TopicsTree;
|
|
@ -210,22 +210,32 @@ export const animateSideView = {
|
|||
|
||||
export const animateSideAppear = {
|
||||
initial: {
|
||||
clipPath: 'inset(0% 100% 0% 0%)'
|
||||
height: 0,
|
||||
opacity: 0
|
||||
},
|
||||
animate: {
|
||||
clipPath: 'inset(0% 0% 0% 0%)',
|
||||
height: 'auto',
|
||||
opacity: 1,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 0.3
|
||||
height: {
|
||||
duration: 0.25
|
||||
},
|
||||
opacity: {
|
||||
delay: 0.25,
|
||||
duration: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
exit: {
|
||||
clipPath: 'inset(0% 100% 0% 0%)',
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 0.3
|
||||
height: {
|
||||
duration: 0.25
|
||||
},
|
||||
opacity: {
|
||||
duration: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user