UI selection improvements

This commit is contained in:
IRBorisov 2024-05-23 13:36:16 +03:00
parent 3391affb72
commit 1a210606d7
13 changed files with 143 additions and 69 deletions

View File

@ -137,9 +137,13 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!thisRef.current?.view) {
event.preventDefault();
event.stopPropagation();
return;
}
if ((event.ctrlKey || event.metaKey) && event.code === 'Space') {
event.preventDefault();
event.stopPropagation();
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.fixSelection(ReferenceTokens);
const nodes = wrap.getEnvelopingNodes(ReferenceTokens);

View File

@ -18,40 +18,54 @@ interface GraphSelectionToolbarProps extends CProps.Styling {
graph: Graph;
core: number[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
emptySelection?: boolean;
}
function GraphSelectionToolbar({ className, graph, core, setSelected, ...restProps }: GraphSelectionToolbarProps) {
function GraphSelectionToolbar({
className,
graph,
core,
setSelected,
emptySelection,
...restProps
}: GraphSelectionToolbarProps) {
return (
<div className={clsx('cc-icons', className)} {...restProps}>
<MiniButton
titleHtml='Сбросить выделение'
icon={<IconReset size='1.25rem' className='icon-primary' />}
onClick={() => setSelected([])}
disabled={emptySelection}
/>
<MiniButton
titleHtml='Выделить все влияющие'
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
onClick={() => setSelected(prev => [...prev, ...graph.expandAllInputs(prev)])}
disabled={emptySelection}
/>
<MiniButton
titleHtml='Выделить все зависимые'
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
onClick={() => setSelected(prev => [...prev, ...graph.expandAllOutputs(prev)])}
disabled={emptySelection}
/>
<MiniButton
titleHtml='<b>Максимизация</b> - дополнение выделения конституентами, зависимыми только от выделенных'
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
onClick={() => setSelected(prev => graph.maximizePart(prev))}
disabled={emptySelection}
/>
<MiniButton
titleHtml='Выделить поставщиков'
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
onClick={() => setSelected(prev => [...prev, ...graph.expandInputs(prev)])}
disabled={emptySelection}
/>
<MiniButton
titleHtml='Выделить потребителей'
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
onClick={() => setSelected(prev => [...prev, ...graph.expandOutputs(prev)])}
disabled={emptySelection}
/>
<MiniButton
titleHtml='Выделить ядро'

View File

@ -84,6 +84,7 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
graph={schema.graph}
core={schema.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)}
setSelected={setSelected}
emptySelection={selected.length === 0}
className='w-full ml-8'
/>
) : null}

View File

@ -37,6 +37,7 @@ function Checkbox({
function handleClick(event: CProps.EventMouse): void {
event.preventDefault();
event.stopPropagation();
if (disabled || !setValue) {
return;
}

View File

@ -35,6 +35,7 @@ function CheckboxTristate({
function handleClick(event: CProps.EventMouse): void {
event.preventDefault();
event.stopPropagation();
if (disabled || !setValue) {
return;
}

View File

@ -2,30 +2,42 @@ import InfoCstStatus from '@/components/info/InfoCstStatus';
import Divider from '@/components/ui/Divider';
import { HelpTopic } from '@/models/miscellaneous';
import { IconAlias, IconMoveDown, IconMoveUp } from '../../../components/Icons';
import { IconAlias, IconDestroy, IconMoveDown, IconMoveUp, IconOpenList, IconReset } from '../../../components/Icons';
import LinkTopic from '../../../components/ui/LinkTopic';
function HelpRSFormItems() {
// prettier-ignore
return (
<div className='dense'>
<h1>Список конституент</h1>
<p><IconAlias className='inline-icon'/>Конституенты обладают уникальным <LinkTopic text='Именем' topic={HelpTopic.CC_CONSTITUENTA}/></p>
<p><IconMoveUp className='inline-icon'/><IconMoveDown className='inline-icon'/> Список поддерживает выделение и перемещение </p>
<div className='dense'>
<h1>Список конституент</h1>
<p>
<IconAlias className='inline-icon' />
Конституенты обладают уникальным <LinkTopic text='Именем' topic={HelpTopic.CC_CONSTITUENTA} />
</p>
<h2>Управление списком</h2>
<li>Клик на строку выделение</li>
<li>Shift + клик выделение нескольких</li>
<li>Alt + клик Редактор</li>
<li>Двойной клик Редактор</li>
<li>Alt + вверх/вниз перемещение</li>
<li>Delete удаление</li>
<li>Alt + 1-6,Q,W добавление</li>
<h2>Управление списком</h2>
<li>
<IconReset className='inline-icon' /> сбросить выделение: ESC
</li>
<li>Клик на строку выделение</li>
<li>Shift + клик выделение нескольких</li>
<li>Alt + клик Редактор</li>
<li>Двойной клик Редактор</li>
<li>
<IconMoveUp className='inline-icon' />
<IconMoveDown className='inline-icon' /> Alt + вверх/вниз перемещение
</li>
<li>
<IconDestroy className='inline-icon icon-red' /> удаление: Delete
</li>
<li>
<IconOpenList className='inline-icon icon-green' /> добавление: Alt + 1-6,Q,W {' '}
</li>
<Divider margins='my-2' />
<Divider margins='my-2' />
<InfoCstStatus title='Статусы' />
</div>);
<InfoCstStatus title='Статусы' />
</div>
);
}
export default HelpRSFormItems;

View File

@ -117,7 +117,7 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
schema={controller.schema}
expression={activeCst?.definition_formal ?? ''}
isBottom={isNarrow}
activeID={activeCst?.id}
activeCst={activeCst}
onOpenEdit={onOpenEdit}
/>
) : null}

View File

@ -50,11 +50,18 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
}
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
controller.deselectAll();
return;
}
if (!controller.isContentEditable || controller.isProcessing) {
return;
}
if (event.key === 'Delete' && controller.selected.length > 0) {
event.preventDefault();
event.stopPropagation();
controller.deleteCst();
return;
}
@ -63,6 +70,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
}
if (processAltKey(event.code)) {
event.preventDefault();
event.stopPropagation();
return;
}
}

View File

@ -1,4 +1,12 @@
import { IconClone, IconDestroy, IconMoveDown, IconMoveUp, IconNewItem, IconOpenList } from '@/components/Icons';
import {
IconClone,
IconDestroy,
IconMoveDown,
IconMoveUp,
IconNewItem,
IconOpenList,
IconReset
} from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
@ -19,6 +27,12 @@ function RSListToolbar() {
return (
<Overlay position='top-1 right-1/2 translate-x-1/2' className='items-start cc-icons'>
<MiniButton
titleHtml={prepareTooltip('Сбросить выделение', 'ESC')}
icon={<IconReset size='1.25rem' className='icon-primary' />}
disabled={controller.selected.length === 0}
onClick={controller.deselectAll}
/>
<MiniButton
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}

View File

@ -186,6 +186,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
}
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
setFocusCst(undefined);
controller.deselectAll();
return;
@ -195,6 +196,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
}
if (event.key === 'Delete') {
event.preventDefault();
event.stopPropagation();
handleDeleteCst();
return;
}
@ -286,7 +288,17 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
);
return (
<>
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
<AnimatePresence>
{showParamsDialog ? (
<DlgGraphParams
hideWindow={() => setShowParamsDialog(false)}
initial={filterParams}
onConfirm={handleChangeParams}
/>
) : null}
</AnimatePresence>
<Overlay
position='top-0 pt-1 right-1/2 translate-x-1/2'
className='flex flex-col items-center rounded-b-2xl cc-blur'
@ -315,6 +327,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
graph={controller.schema!.graph}
core={controller.schema!.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)}
setSelected={controller.setSelected}
emptySelection={controller.selected.length === 0}
/>
) : null}
{focusCst ? (
@ -338,44 +351,33 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
/>
) : null}
</Overlay>
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
<AnimatePresence>
{showParamsDialog ? (
<DlgGraphParams
hideWindow={() => setShowParamsDialog(false)}
initial={filterParams}
onConfirm={handleChangeParams}
/>
) : null}
</AnimatePresence>
<SelectedCounter
hideZero
totalCount={controller.schema?.stats?.count_all ?? 0}
selectedCount={controller.selected.length}
position='top-[4.3rem] sm:top-[0.3rem] left-0'
/>
<SelectedCounter
hideZero
totalCount={controller.schema?.stats?.count_all ?? 0}
selectedCount={controller.selected.length}
position='top-[4.3rem] sm:top-[0.3rem] left-0'
/>
{hoverCst && hoverCstDebounced && hoverCst === hoverCstDebounced ? (
<Overlay
layer='z-tooltip'
position={clsx('top-[1.6rem]', { 'left-[2.6rem]': hoverLeft, 'right-[2.6rem]': !hoverLeft })}
className={clsx('w-[25rem]', 'px-3', 'cc-scroll-y', 'border shadow-md', 'clr-app')}
>
<InfoConstituenta className='pt-1 pb-2' data={hoverCstDebounced} />
</Overlay>
) : null}
<Overlay position='top-[6.25rem] sm:top-9 left-0' className='flex gap-1'>
<div className='flex flex-col ml-2 w-[13.5rem]'>
{selectors}
{viewHidden}
</div>
{hoverCst && hoverCstDebounced && hoverCst === hoverCstDebounced ? (
<Overlay
layer='z-tooltip'
position={clsx('top-[1.6rem]', { 'left-[2.6rem]': hoverLeft, 'right-[2.6rem]': !hoverLeft })}
className={clsx('w-[25rem]', 'px-3', 'cc-scroll-y', 'border shadow-md', 'clr-app')}
>
<InfoConstituenta className='pt-1 pb-2' data={hoverCstDebounced} />
</Overlay>
) : null}
{graph}
</AnimateFade>
</>
<Overlay position='top-[6.25rem] sm:top-9 left-0' className='flex gap-1'>
<div className='flex flex-col ml-2 w-[13.5rem]'>
{selectors}
{viewHidden}
</div>
</Overlay>
{graph}
</AnimateFade>
);
}

View File

@ -114,14 +114,14 @@ function RSTabs() {
const onCreateCst = useCallback(
(newCst: IConstituentaMeta) => {
navigateTab(activeTab, newCst.id);
if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) {
if (activeTab === RSTabID.CST_LIST) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
inline: 'end'
});
}
}, PARAMETER.refreshTimeout);

View File

@ -9,12 +9,12 @@ import { useConceptOptions } from '@/context/OptionsContext';
import useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta } from '@/models/rsform';
import { isMockCst } from '@/models/rsformAPI';
import { prefixes } from '@/utils/constants';
import { PARAMETER, prefixes } from '@/utils/constants';
import { describeConstituenta } from '@/utils/labels';
interface ConstituentsTableProps {
items: IConstituenta[];
activeID?: ConstituentaID;
activeCst?: IConstituenta;
onOpenEdit: (cstID: ConstituentaID) => void;
denseThreshold?: number;
maxHeight: string;
@ -22,12 +22,29 @@ interface ConstituentsTableProps {
const columnHelper = createColumnHelper<IConstituenta>();
function ConstituentsTable({ items, activeID, onOpenEdit, maxHeight, denseThreshold = 9999 }: ConstituentsTableProps) {
function ConstituentsTable({ items, activeCst, onOpenEdit, maxHeight, denseThreshold = 9999 }: ConstituentsTableProps) {
const { colors } = useConceptOptions();
const windowSize = useWindowSize();
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({ expression: true });
useLayoutEffect(() => {
if (!activeCst) {
return;
}
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.alias}`);
console.log(element);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'end'
});
}
}, PARAMETER.refreshTimeout);
}, [activeCst]);
useLayoutEffect(() => {
setColumnVisibility(prev => {
const newValue = (windowSize.width ?? 0) >= denseThreshold;
@ -104,25 +121,25 @@ function ConstituentsTable({ items, activeID, onOpenEdit, maxHeight, denseThresh
const conditionalRowStyles = useMemo(
(): IConditionalStyle<IConstituenta>[] => [
{
when: (cst: IConstituenta) => cst.id === activeID,
when: (cst: IConstituenta) => cst.id === activeCst?.id,
style: {
backgroundColor: colors.bgSelected
}
},
{
when: (cst: IConstituenta) => cst.parent === activeID && cst.id !== activeID,
when: (cst: IConstituenta) => cst.parent === activeCst?.id && cst.id !== activeCst?.id,
style: {
backgroundColor: colors.bgOrange50
}
},
{
when: (cst: IConstituenta) => activeID !== undefined && cst.children.includes(activeID),
when: (cst: IConstituenta) => activeCst?.id !== undefined && cst.children.includes(activeCst.id),
style: {
backgroundColor: colors.bgGreen50
}
}
],
[activeID, colors]
[activeCst, colors]
);
return (

View File

@ -17,12 +17,12 @@ const COLUMN_EXPRESSION_HIDE_THRESHOLD = 1500;
interface ViewConstituentsProps {
expression: string;
isBottom?: boolean;
activeID?: ConstituentaID;
activeCst?: IConstituenta;
schema?: IRSForm;
onOpenEdit: (cstID: ConstituentaID) => void;
}
function ViewConstituents({ expression, schema, activeID, isBottom, onOpenEdit }: ViewConstituentsProps) {
function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit }: ViewConstituentsProps) {
const { calculateHeight } = useConceptOptions();
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
@ -32,12 +32,12 @@ function ViewConstituents({ expression, schema, activeID, isBottom, onOpenEdit }
<ConstituentsTable
maxHeight={isBottom ? '12rem' : calculateHeight('8.2rem')}
items={filteredData}
activeID={activeID}
activeCst={activeCst}
onOpenEdit={onOpenEdit}
denseThreshold={COLUMN_EXPRESSION_HIDE_THRESHOLD}
/>
),
[isBottom, filteredData, activeID, onOpenEdit, calculateHeight]
[isBottom, filteredData, activeCst, onOpenEdit, calculateHeight]
);
return (
@ -55,7 +55,7 @@ function ViewConstituents({ expression, schema, activeID, isBottom, onOpenEdit }
>
<ConstituentsSearch
schema={schema}
activeID={activeID}
activeID={activeCst?.id}
activeExpression={expression}
setFiltered={setFilteredData}
/>