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>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!thisRef.current?.view) { if (!thisRef.current?.view) {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
return; return;
} }
if ((event.ctrlKey || event.metaKey) && event.code === 'Space') { if ((event.ctrlKey || event.metaKey) && event.code === 'Space') {
event.preventDefault();
event.stopPropagation();
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>); const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.fixSelection(ReferenceTokens); wrap.fixSelection(ReferenceTokens);
const nodes = wrap.getEnvelopingNodes(ReferenceTokens); const nodes = wrap.getEnvelopingNodes(ReferenceTokens);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,11 +50,18 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
} }
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
controller.deselectAll();
return;
}
if (!controller.isContentEditable || controller.isProcessing) { if (!controller.isContentEditable || controller.isProcessing) {
return; return;
} }
if (event.key === 'Delete' && controller.selected.length > 0) { if (event.key === 'Delete' && controller.selected.length > 0) {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
controller.deleteCst(); controller.deleteCst();
return; return;
} }
@ -63,6 +70,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
} }
if (processAltKey(event.code)) { if (processAltKey(event.code)) {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
return; 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 BadgeHelp from '@/components/info/BadgeHelp';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
@ -19,6 +27,12 @@ function RSListToolbar() {
return ( return (
<Overlay position='top-1 right-1/2 translate-x-1/2' className='items-start cc-icons'> <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 <MiniButton
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')} titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<IconMoveUp size='1.25rem' className='icon-primary' />} icon={<IconMoveUp size='1.25rem' className='icon-primary' />}

View File

@ -186,6 +186,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
} }
if (event.key === 'Escape') { if (event.key === 'Escape') {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
setFocusCst(undefined); setFocusCst(undefined);
controller.deselectAll(); controller.deselectAll();
return; return;
@ -195,6 +196,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
} }
if (event.key === 'Delete') { if (event.key === 'Delete') {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
handleDeleteCst(); handleDeleteCst();
return; return;
} }
@ -286,7 +288,17 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
); );
return ( return (
<> <AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
<AnimatePresence>
{showParamsDialog ? (
<DlgGraphParams
hideWindow={() => setShowParamsDialog(false)}
initial={filterParams}
onConfirm={handleChangeParams}
/>
) : null}
</AnimatePresence>
<Overlay <Overlay
position='top-0 pt-1 right-1/2 translate-x-1/2' position='top-0 pt-1 right-1/2 translate-x-1/2'
className='flex flex-col items-center rounded-b-2xl cc-blur' className='flex flex-col items-center rounded-b-2xl cc-blur'
@ -315,6 +327,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
graph={controller.schema!.graph} graph={controller.schema!.graph}
core={controller.schema!.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)} core={controller.schema!.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)}
setSelected={controller.setSelected} setSelected={controller.setSelected}
emptySelection={controller.selected.length === 0}
/> />
) : null} ) : null}
{focusCst ? ( {focusCst ? (
@ -338,16 +351,6 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
/> />
) : null} ) : null}
</Overlay> </Overlay>
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
<AnimatePresence>
{showParamsDialog ? (
<DlgGraphParams
hideWindow={() => setShowParamsDialog(false)}
initial={filterParams}
onConfirm={handleChangeParams}
/>
) : null}
</AnimatePresence>
<SelectedCounter <SelectedCounter
hideZero hideZero
@ -375,7 +378,6 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
{graph} {graph}
</AnimateFade> </AnimateFade>
</>
); );
} }

View File

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

View File

@ -9,12 +9,12 @@ import { useConceptOptions } from '@/context/OptionsContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta } from '@/models/rsform'; import { ConstituentaID, IConstituenta } from '@/models/rsform';
import { isMockCst } from '@/models/rsformAPI'; import { isMockCst } from '@/models/rsformAPI';
import { prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { describeConstituenta } from '@/utils/labels'; import { describeConstituenta } from '@/utils/labels';
interface ConstituentsTableProps { interface ConstituentsTableProps {
items: IConstituenta[]; items: IConstituenta[];
activeID?: ConstituentaID; activeCst?: IConstituenta;
onOpenEdit: (cstID: ConstituentaID) => void; onOpenEdit: (cstID: ConstituentaID) => void;
denseThreshold?: number; denseThreshold?: number;
maxHeight: string; maxHeight: string;
@ -22,12 +22,29 @@ interface ConstituentsTableProps {
const columnHelper = createColumnHelper<IConstituenta>(); 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 { colors } = useConceptOptions();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({ expression: true }); 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(() => { useLayoutEffect(() => {
setColumnVisibility(prev => { setColumnVisibility(prev => {
const newValue = (windowSize.width ?? 0) >= denseThreshold; const newValue = (windowSize.width ?? 0) >= denseThreshold;
@ -104,25 +121,25 @@ function ConstituentsTable({ items, activeID, onOpenEdit, maxHeight, denseThresh
const conditionalRowStyles = useMemo( const conditionalRowStyles = useMemo(
(): IConditionalStyle<IConstituenta>[] => [ (): IConditionalStyle<IConstituenta>[] => [
{ {
when: (cst: IConstituenta) => cst.id === activeID, when: (cst: IConstituenta) => cst.id === activeCst?.id,
style: { style: {
backgroundColor: colors.bgSelected backgroundColor: colors.bgSelected
} }
}, },
{ {
when: (cst: IConstituenta) => cst.parent === activeID && cst.id !== activeID, when: (cst: IConstituenta) => cst.parent === activeCst?.id && cst.id !== activeCst?.id,
style: { style: {
backgroundColor: colors.bgOrange50 backgroundColor: colors.bgOrange50
} }
}, },
{ {
when: (cst: IConstituenta) => activeID !== undefined && cst.children.includes(activeID), when: (cst: IConstituenta) => activeCst?.id !== undefined && cst.children.includes(activeCst.id),
style: { style: {
backgroundColor: colors.bgGreen50 backgroundColor: colors.bgGreen50
} }
} }
], ],
[activeID, colors] [activeCst, colors]
); );
return ( return (

View File

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