mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
UI selection improvements
This commit is contained in:
parent
3391affb72
commit
1a210606d7
|
@ -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);
|
||||
|
|
|
@ -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='Выделить ядро'
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -37,6 +37,7 @@ function Checkbox({
|
|||
|
||||
function handleClick(event: CProps.EventMouse): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (disabled || !setValue) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ function CheckboxTristate({
|
|||
|
||||
function handleClick(event: CProps.EventMouse): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (disabled || !setValue) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' />}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue
Block a user