Rework Search panels UI

This commit is contained in:
IRBorisov 2023-10-06 14:39:32 +03:00
parent a44a43214c
commit 8d062baa8b
11 changed files with 230 additions and 213 deletions

View File

@ -12,7 +12,7 @@ For more specific TODOs see comments in code
- система обработки ошибок backend
[Tech]
- reload react-data-table-component
- multilevel modals / rework modal system
[deployment]
- logs collection

View File

@ -1,5 +1,5 @@
interface ButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'> {
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'| 'type'> {
text?: string
icon?: React.ReactNode
tooltip?: string
@ -11,21 +11,19 @@ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'child
}
function Button({
id, text, icon, tooltip,
text, icon, tooltip,
dense, disabled,
borderClass = 'border rounded',
colorClass = 'clr-btn-default',
dimensions = 'w-fit h-fit',
loading, onClick,
loading,
...props
}: ButtonProps) {
const padding = dense ? 'px-1' : 'px-3 py-2';
const cursor = 'disabled:cursor-not-allowed ' + (loading ? 'cursor-progress ' : 'cursor-pointer ');
return (
<button id={id}
type='button'
<button type='button'
disabled={disabled ?? loading}
onClick={onClick}
title={tooltip}
className={`inline-flex items-center gap-2 align-middle justify-center select-none ${padding} ${colorClass} ${dimensions} ${borderClass} ${cursor}`}
{...props}

View File

@ -0,0 +1,34 @@
interface SelectorButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title' | 'type'> {
text?: string
icon?: React.ReactNode
tooltip?: string
dimensions?: string
borderClass?: string
colorClass?: string
transparent?: boolean
}
function SelectorButton({
text, icon, tooltip,
colorClass = 'clr-btn-default',
dimensions = 'w-fit h-fit',
transparent,
...props
}: SelectorButtonProps) {
const cursor = 'disabled:cursor-not-allowed cursor-pointer';
const position = `px-1 flex flex-start items-center gap-1 ${dimensions}`
return (
<button type='button'
className={`text-sm small-caps ${!transparent && 'border'} ${cursor} ${position} text-btn text-controls ${!transparent && colorClass}`}
title={tooltip}
{...props}
>
{icon && icon}
{text && <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div>}
</button>
);
}
export default SelectorButton;

View File

@ -16,7 +16,7 @@ import SelectMulti from '../Common/SelectMulti';
import TextInput from '../Common/TextInput';
import DataTable, { IConditionalStyle } from '../DataTable';
import HelpTerminologyControl from '../Help/HelpTerminologyControl';
import { HelpIcon } from '../Icons';
import { HelpIcon, MagnifyingGlassIcon } from '../Icons';
import ReferenceTypeButton from './ReferenceTypeButton';
import WordformButton from './WordformButton';
@ -284,12 +284,17 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
</div>}
{type === ReferenceType.ENTITY &&
<div className='flex flex-col gap-2'>
<div className='relative'>
<div className='absolute inset-y-0 flex items-center pl-3 pointer-events-none text-controls'>
<MagnifyingGlassIcon />
</div>
<TextInput
dimensions='w-full'
placeholder='текст фильтра'
dimensions='w-full pl-10'
placeholder='Поиск'
value={filter}
onChange={event => setFilter(event.target.value)}
/>
</div>
<div className='border min-h-[15.5rem] max-h-[15.5rem] text-sm overflow-y-auto'>
<DataTable
data={filteredData}
@ -311,8 +316,9 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
</div>
<div className='flex gap-4 flex-start'>
<TextInput
label='Отсылаемый идентификатор'
dimensions='max-w-[18rem] min-w-[18rem] whitespace-nowrap'
label='Отсылаемая конституента'
dimensions='max-w-[16rem] min-w-[16rem] whitespace-nowrap'
placeholder='Имя'
singleRow
value={alias}
onChange={event => setAlias(event.target.value)}

View File

@ -1,12 +1,14 @@
import { useCallback } from 'react';
import Button from '../../components/Common/Button';
import Dropdown from '../../components/Common/Dropdown';
import DropdownCheckbox from '../../components/Common/DropdownCheckbox';
import SelectorButton from '../../components/Common/SelectorButton';
import { FilterCogIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext';
import useDropdown from '../../hooks/useDropdown';
import { LibraryFilterStrategy } from '../../models/miscelanious';
import { prefixes } from '../../utils/constants';
import { describeLibraryFilter, labelLibraryFilter } from '../../utils/labels';
interface PickerStrategyProps {
value: LibraryFilterStrategy
@ -14,65 +16,52 @@ interface PickerStrategyProps {
}
function PickerStrategy({ value, onChange }: PickerStrategyProps) {
const pickerMenu = useDropdown();
const strategyMenu = useDropdown();
const { user } = useAuth();
const handleChange = useCallback(
(newValue: LibraryFilterStrategy) => {
pickerMenu.hide();
strategyMenu.hide();
onChange(newValue);
}, [pickerMenu, onChange]);
}, [strategyMenu, onChange]);
function isStrategyDisabled(strategy: LibraryFilterStrategy): boolean {
if (
strategy === LibraryFilterStrategy.PERSONAL ||
strategy === LibraryFilterStrategy.SUBSCRIBE ||
strategy === LibraryFilterStrategy.OWNED
) {
return !user;
} else {
return false;
}
}
return (
<div ref={pickerMenu.ref} className='h-full text-right'>
<Button
icon={<FilterCogIcon color='text-controls' size={6} />}
dense
tooltip='Фильтры'
colorClass='clr-input clr-hover text-btn'
dimensions='h-full py-1 px-2 border-none'
onClick={pickerMenu.toggle}
<div ref={strategyMenu.ref} className='h-full text-right'>
<SelectorButton
tooltip='Список фильтров'
transparent
icon={<FilterCogIcon size={5} />}
text={labelLibraryFilter(value)}
tabIndex={-1}
onClick={strategyMenu.toggle}
/>
{ pickerMenu.isActive &&
{ strategyMenu.isActive &&
<Dropdown>
{ Object.values(LibraryFilterStrategy).map(
(enumValue, index) => {
const strategy = enumValue as LibraryFilterStrategy;
return (
<DropdownCheckbox
setValue={() => handleChange(LibraryFilterStrategy.MANUAL)}
value={value === LibraryFilterStrategy.MANUAL}
label='Отображать все'
/>
<DropdownCheckbox
setValue={() => handleChange(LibraryFilterStrategy.COMMON)}
value={value === LibraryFilterStrategy.COMMON}
label='Общедоступные'
tooltip='Отображать только общедоступные схемы'
/>
<DropdownCheckbox
setValue={() => handleChange(LibraryFilterStrategy.CANONICAL)}
value={value === LibraryFilterStrategy.CANONICAL}
label='Неизменные'
tooltip='Отображать только стандартные схемы'
/>
<DropdownCheckbox
setValue={() => handleChange(LibraryFilterStrategy.PERSONAL)}
value={value === LibraryFilterStrategy.PERSONAL}
label='Личные'
disabled={!user}
tooltip='Отображать только подписки и владеемые схемы'
/>
<DropdownCheckbox
setValue={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)}
value={value === LibraryFilterStrategy.SUBSCRIBE}
label='Подписки'
disabled={!user}
tooltip='Отображать только подписки'
/>
<DropdownCheckbox
setValue={() => handleChange(LibraryFilterStrategy.OWNED)}
value={value === LibraryFilterStrategy.OWNED}
disabled={!user}
label='Я - Владелец!'
tooltip='Отображать только владеемые схемы'
/>
key={`${prefixes.library_filters_list}${index}`}
value={value === strategy}
setValue={() => handleChange(strategy)}
label={labelLibraryFilter(strategy)}
tooltip={describeLibraryFilter(strategy)}
disabled={isStrategyDisabled(strategy)}
/>);
})}
</Dropdown>}
</div>
);

View File

@ -76,12 +76,8 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setStrategy,
{filtered} из {total}
</span>
</div>
<div className='flex items-center justify-center w-full ml-8'>
<PickerStrategy
value={strategy}
onChange={handleChangeStrategy}
/>
<div className='relative w-96 min-w-[10rem] select-none'>
<div className='flex items-center justify-center w-full gap-1'>
<div className='relative min-w-[10rem] select-none'>
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-controls'>
<MagnifyingGlassIcon />
</div>
@ -89,10 +85,14 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setStrategy,
type='text'
value={query}
className='w-full p-2 pl-10 text-sm outline-none clr-input'
placeholder='Поиск схемы...'
placeholder='Поиск'
onChange={handleChangeQuery}
/>
</div>
<PickerStrategy
value={strategy}
onChange={handleChangeStrategy}
/>
</div>
</div>
);

View File

@ -1,60 +0,0 @@
import { useCallback } from 'react';
import Dropdown from '../../../components/Common/Dropdown';
import DropdownButton from '../../../components/Common/DropdownButton';
import { CogIcon } from '../../../components/Icons';
import useDropdown from '../../../hooks/useDropdown';
import { DependencyMode } from '../../../models/miscelanious';
import { labelDependencyMode } from '../../../utils/labels';
interface DependencyModePickerProps {
value: DependencyMode
onChange: (value: DependencyMode) => void
}
function DependencyModePicker({ value, onChange }: DependencyModePickerProps) {
const pickerMenu = useDropdown();
const handleChange = useCallback(
(newValue: DependencyMode) => {
pickerMenu.hide();
onChange(newValue);
}, [pickerMenu, onChange]);
return (
<div ref={pickerMenu.ref} className='h-full'>
<button
className='h-full w-[7.5rem] px-1 py-1 border clr-input clr-hover clr-btn-default text-btn inline-flex align-middle gap-1'
title='Настройка фильтрации по графу термов'
tabIndex={-1}
onClick={pickerMenu.toggle}
>
<CogIcon color='text-controls' size={5} />
<span className='text-sm font-semibold whitespace-nowrap'>{labelDependencyMode(value)}</span>
</button>
{ pickerMenu.isActive &&
<Dropdown stretchLeft >
<DropdownButton onClick={() => handleChange(DependencyMode.ALL)}>
<p><b>вся схема:</b> список всех конституент схемы</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.EXPRESSION)}>
<p><b>выражение:</b> список идентификаторов из выражения</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.OUTPUTS)}>
<p><b>потребители:</b> конституенты, ссылающиеся на данную</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.INPUTS)}>
<p><b>поставщики:</b> конституенты, на которые ссылается данная</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.EXPAND_OUTPUTS)}>
<p><b>зависимые:</b> конституенты, зависящие по цепочке</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.EXPAND_INPUTS)}>
<p><b>влияющие:</b> конституенты, влияющие на данную (цепочка)</p>
</DropdownButton>
</Dropdown>}
</div>
);
}
export default DependencyModePicker;

View File

@ -1,58 +0,0 @@
import { useCallback } from 'react';
import Dropdown from '../../../components/Common/Dropdown';
import DropdownButton from '../../../components/Common/DropdownButton';
import { FilterCogIcon } from '../../../components/Icons';
import useDropdown from '../../../hooks/useDropdown';
import { CstMatchMode } from '../../../models/miscelanious';
import { labelCstMathchMode } from '../../../utils/labels';
interface MatchModePickerProps {
value: CstMatchMode
onChange: (value: CstMatchMode) => void
}
function MatchModePicker({ value, onChange }: MatchModePickerProps) {
const pickerMenu = useDropdown();
const handleChange = useCallback(
(newValue: CstMatchMode) => {
pickerMenu.hide();
onChange(newValue);
}, [pickerMenu, onChange]);
return (
<div ref={pickerMenu.ref} className='h-full'>
<button
className='h-full w-[6rem] px-1 py-1 border clr-input clr-hover clr-btn-default text-btn inline-flex align-middle gap-1'
title='Настройка атрибутов для фильтрации'
tabIndex={-1}
onClick={pickerMenu.toggle}
>
<FilterCogIcon color='text-controls' size={5} />
<span className='text-sm font-semibold whitespace-nowrap'>{labelCstMathchMode(value)}</span>
</button>
{ pickerMenu.isActive &&
<Dropdown stretchLeft>
<DropdownButton onClick={() => handleChange(CstMatchMode.ALL)}>
<p><b>везде:</b> искать во всех атрибутах</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(CstMatchMode.EXPR)}>
<p><b>выраж:</b> искать в формальных выражениях</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(CstMatchMode.TERM)}>
<p><b>термин:</b> искать в терминах</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(CstMatchMode.TEXT)}>
<p><b>текст:</b> искать в определениях и конвенциях</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(CstMatchMode.NAME)}>
<p><b>имя:</b> искать в идентификаторах конституент</p>
</DropdownButton>
</Dropdown>
}
</div>
);
}
export default MatchModePicker;

View File

@ -1,22 +1,28 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import Dropdown from '../../../components/Common/Dropdown';
import DropdownButton from '../../../components/Common/DropdownButton';
import SelectorButton from '../../../components/Common/SelectorButton';
import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } from '../../../components/DataTable';
import { MagnifyingGlassIcon } from '../../../components/Icons';
import { CogIcon, FilterCogIcon, MagnifyingGlassIcon } from '../../../components/Icons';
import { useRSForm } from '../../../context/RSFormContext';
import { useConceptTheme } from '../../../context/ThemeContext';
import useDropdown from '../../../hooks/useDropdown';
import useLocalStorage from '../../../hooks/useLocalStorage';
import useWindowSize from '../../../hooks/useWindowSize';
import { DependencyMode } from '../../../models/miscelanious';
import { DependencyMode as CstSource } from '../../../models/miscelanious';
import { CstMatchMode } from '../../../models/miscelanious';
import { applyGraphFilter } from '../../../models/miscelanious';
import { CstType, extractGlobals, IConstituenta, matchConstituenta } from '../../../models/rsform';
import { createMockConstituenta } from '../../../models/rsform';
import { colorfgCstStatus } from '../../../utils/color';
import { prefixes } from '../../../utils/constants';
import { describeConstituenta } from '../../../utils/labels';
import {
describeConstituenta, describeCstMathchMode,
describeCstSource, labelCstMathchMode,
labelCstSource
} from '../../../utils/labels';
import ConstituentaTooltip from './ConstituentaTooltip';
import DependencyModePicker from './DependencyModePicker';
import MatchModePicker from './MatchModePicker';
// Height that should be left to accomodate navigation panel + bottom margin
const LOCAL_NAVIGATION_H = '2.1rem';
@ -46,10 +52,13 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '');
const [filterSource, setFilterSource] = useLocalStorage('side-filter-dependency', DependencyMode.ALL);
const [filterSource, setFilterSource] = useLocalStorage('side-filter-dependency', CstSource.ALL);
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
const matchModeMenu = useDropdown();
const sourceMenu = useDropdown();
useLayoutEffect(
() => {
setColumnVisibility(prev => {
@ -69,7 +78,7 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
return;
}
let filtered: IConstituenta[] = [];
if (filterSource === DependencyMode.EXPRESSION) {
if (filterSource === CstSource.EXPRESSION) {
const aliases = extractGlobals(expression);
filtered = schema.items.filter((cst) => aliases.has(cst.alias));
const names = filtered.map(cst => cst.alias)
@ -113,6 +122,18 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
}
}, [onOpenEdit]);
const handleMatchModeChange = useCallback(
(newValue: CstMatchMode) => {
matchModeMenu.hide();
setFilterMatch(newValue);
}, [matchModeMenu, setFilterMatch]);
const handleSourceChange = useCallback(
(newValue: CstSource) => {
sourceMenu.hide();
setFilterSource(newValue);
}, [sourceMenu, setFilterSource]);
const columns = useMemo(
() => [
columnHelper.accessor('alias', {
@ -196,18 +217,59 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
</div>
<input type='text'
className='w-[14rem] pr-2 pl-8 py-1 outline-none select-none hover:text-clip clr-input'
placeholder='текст фильтра'
placeholder='Поиск'
value={filterText}
onChange={event => setFilterText(event.target.value)}
/>
<MatchModePicker
value={filterMatch}
onChange={setFilterMatch}
<div ref={matchModeMenu.ref}>
<SelectorButton
tooltip='Настройка атрибутов для фильтрации'
transparent
icon={<FilterCogIcon size={5} />}
text={labelCstMathchMode(filterMatch)}
tabIndex={-1}
onClick={matchModeMenu.toggle}
/>
<DependencyModePicker
value={filterSource}
onChange={setFilterSource}
{ matchModeMenu.isActive &&
<Dropdown stretchLeft>
{ Object.values(CstMatchMode).filter(value => !isNaN(Number(value))).map(
(value, index) => {
const matchMode = value as CstMatchMode;
return (
<DropdownButton
key={`${prefixes.cst_match_mode_list}${index}`}
onClick={() => handleMatchModeChange(matchMode)}
>
<p><span className='font-semibold'>{labelCstMathchMode(matchMode)}:</span> {describeCstMathchMode(matchMode)}</p>
</DropdownButton>);
})}
</Dropdown>}
</div>
<div ref={sourceMenu.ref}>
<SelectorButton
tooltip='Настройка фильтрации по графу термов'
transparent
icon={<CogIcon size={4} />}
text={labelCstSource(filterSource)}
tabIndex={-1}
onClick={sourceMenu.toggle}
/>
{ sourceMenu.isActive &&
<Dropdown stretchLeft>
{ Object.values(CstSource).filter(value => !isNaN(Number(value))).map(
(value, index) => {
const source = value as CstSource;
return (
<DropdownButton
key={`${prefixes.cst_source_list}${index}`}
onClick={() => handleSourceChange(source)}
>
<p><span className='font-semibold'>{labelCstSource(source)}:</span> {describeCstSource(source)}</p>
</DropdownButton>);
})}
</Dropdown>}
</div>
</div>
<div className='overflow-y-auto text-sm' style={{maxHeight : `${maxHeight}`}}>
<DataTable

View File

@ -36,6 +36,9 @@ export const prefixes = {
cst_list: 'cst-list-',
cst_wordform_list: 'cst-wordform-list-',
cst_status_list: 'cst-status-list-',
cst_match_mode_list: 'cst-match-mode-list-',
cst_source_list: 'cst-source-list-',
library_filters_list: 'library-filters-list',
topic_list: 'topic-list-',
library_list: 'library-list-',
wordform_list: 'wordform-list'

View File

@ -1,7 +1,7 @@
// =========== Modules contains all text descriptors ==========
import { GramData,Grammeme, ReferenceType } from '../models/language';
import { CstMatchMode, DependencyMode, HelpTopic } from '../models/miscelanious';
import { CstMatchMode, DependencyMode, HelpTopic, LibraryFilterStrategy } from '../models/miscelanious';
import { CstClass, CstType, ExpressionStatus, IConstituenta } from '../models/rsform';
import { IFunctionArg, IRSErrorDescription, ISyntaxTreeNode, ParsingStatus, RSErrorType, TokenID } from '../models/rslang';
@ -128,17 +128,27 @@ export function describeToken(id: TokenID): string {
export function labelCstMathchMode(mode: CstMatchMode): string {
switch (mode) {
case CstMatchMode.ALL: return 'везде';
case CstMatchMode.EXPR: return 'выраж';
case CstMatchMode.ALL: return 'общий';
case CstMatchMode.EXPR: return 'выражение';
case CstMatchMode.TERM: return 'термин';
case CstMatchMode.TEXT: return 'текст';
case CstMatchMode.NAME: return 'имя';
}
}
export function labelDependencyMode(mode: DependencyMode): string {
export function describeCstMathchMode(mode: CstMatchMode): string {
switch (mode) {
case DependencyMode.ALL: return 'вся схема';
case CstMatchMode.ALL: return 'искать во всех атрибутах';
case CstMatchMode.EXPR: return 'искать в формальных выражениях';
case CstMatchMode.TERM: return 'искать в терминах';
case CstMatchMode.TEXT: return 'искать в определениях и конвенциях';
case CstMatchMode.NAME: return 'искать в идентификаторах конституент';
}
}
export function labelCstSource(mode: DependencyMode): string {
switch (mode) {
case DependencyMode.ALL: return 'не ограничен';
case DependencyMode.EXPRESSION: return 'выражение';
case DependencyMode.OUTPUTS: return 'потребители';
case DependencyMode.INPUTS: return 'поставщики';
@ -147,6 +157,39 @@ export function labelDependencyMode(mode: DependencyMode): string {
}
}
export function describeCstSource(mode: DependencyMode): string {
switch (mode) {
case DependencyMode.ALL: return 'все конституенты';
case DependencyMode.EXPRESSION: return 'идентификаторы из выражения';
case DependencyMode.OUTPUTS: return 'конституенты, ссылающиеся на данную';
case DependencyMode.INPUTS: return 'конституенты, на которые ссылается данная';
case DependencyMode.EXPAND_INPUTS: return 'конституенты, зависящие по цепочке';
case DependencyMode.EXPAND_OUTPUTS: return 'конституенты, влияющие на данную по цепочке';
}
}
export function labelLibraryFilter(strategy: LibraryFilterStrategy): string {
switch (strategy) {
case LibraryFilterStrategy.MANUAL: return 'отображать все';
case LibraryFilterStrategy.COMMON: return 'общедоступные';
case LibraryFilterStrategy.CANONICAL: return 'неизменные';
case LibraryFilterStrategy.PERSONAL: return 'личные';
case LibraryFilterStrategy.SUBSCRIBE: return 'подписки';
case LibraryFilterStrategy.OWNED: return 'владелец';
}
}
export function describeLibraryFilter(strategy: LibraryFilterStrategy): string {
switch (strategy) {
case LibraryFilterStrategy.MANUAL: return 'Отображать все схемы';
case LibraryFilterStrategy.COMMON: return 'Отображать общедоступные схемы';
case LibraryFilterStrategy.CANONICAL: return 'Отображать стандартные схемы';
case LibraryFilterStrategy.PERSONAL: return 'Отображать подписки и владеемые схемы';
case LibraryFilterStrategy.SUBSCRIBE: return 'Отображать подписки';
case LibraryFilterStrategy.OWNED: return 'Отображать владеемые схемы';
}
}
export const mapLableLayout: Map<string, string> =
new Map([
['forceatlas2', 'Граф: Атлас 2D'],