Refactoring: improve id's and labels

Do not use html labels without inputs
This commit is contained in:
IRBorisov 2024-03-18 16:21:39 +03:00
parent 38cd91765a
commit 56cb7236ca
37 changed files with 250 additions and 110 deletions

View File

@ -13,6 +13,7 @@ import ConstituentaBadge from './ConstituentaBadge';
import FlexColumn from './ui/FlexColumn';
interface ConstituentaPickerProps {
id?: string;
prefixID?: string;
data?: IConstituenta[];
rows?: number;
@ -29,6 +30,7 @@ interface ConstituentaPickerProps {
const columnHelper = createColumnHelper<IConstituenta>();
function ConstituentaPicker({
id,
data,
value,
initialFilter = '',
@ -85,13 +87,19 @@ function ConstituentaPicker({
);
return (
<div>
<SearchBar value={filterText} onChange={newValue => setFilterText(newValue)} />
<div className='border divide-y'>
<SearchBar
id={id ? `${id}__search` : undefined}
noBorder
value={filterText}
onChange={newValue => setFilterText(newValue)}
/>
<DataTable
id={id}
dense
noHeader
noFooter
className='overflow-y-auto text-sm border select-none'
className='overflow-y-auto text-sm select-none'
style={{ maxHeight: size, minHeight: size }}
data={filteredData}
columns={columns}

View File

@ -9,7 +9,7 @@ interface ConstituentaTooltipProps {
function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
return (
<Tooltip clickable anchorSelect={anchor} className='max-w-[30rem]'>
<Tooltip clickable layer='z-modal-tooltip' anchorSelect={anchor} className='max-w-[30rem]'>
<InfoConstituenta data={data} onClick={event => event.stopPropagation()} />
</Tooltip>
);

View File

@ -14,7 +14,7 @@ import {
useReactTable,
type VisibilityState
} from '@tanstack/react-table';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { CProps } from '../props';
import DefaultNoData from './DefaultNoData';
@ -33,7 +33,9 @@ export interface IConditionalStyle<TData> {
export interface DataTableProps<TData extends RowData>
extends CProps.Styling,
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
id?: string;
dense?: boolean;
rows?: number;
headPosition?: string;
noHeader?: boolean;
noFooter?: boolean;
@ -66,9 +68,11 @@ export interface DataTableProps<TData extends RowData>
* No sticky header if omitted
*/
function DataTable<TData extends RowData>({
id,
style,
className,
dense,
rows,
headPosition,
conditionalRowStyles,
noFooter,
@ -120,8 +124,20 @@ function DataTable<TData extends RowData>({
const isEmpty = tableImpl.getRowModel().rows.length === 0;
// TODO: refactor formula for different font sizes and pagination tools
const fixedSize = useMemo(() => {
if (!rows) {
return undefined;
}
if (dense) {
return `calc(2px + (2px + 1.6875rem)*${rows} + ${noHeader ? '0px' : '(2px + 2.1875rem)'})`;
} else {
return `calc(2px + (2px + 2.1875rem)*${rows + (noHeader ? 0 : 1)})`;
}
}, [rows, dense, noHeader]);
return (
<div className={className} style={style}>
<div id={id} className={className} style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}>
<table className='w-full'>
{!noHeader ? (
<TableHeader
@ -146,6 +162,7 @@ function DataTable<TData extends RowData>({
{enablePagination && !isEmpty ? (
<PaginationTools
id={id ? `${id}__pagination` : undefined}
table={tableImpl}
paginationOptions={paginationOptions}
onChangePaginationOption={onChangePaginationOption}

View File

@ -8,12 +8,18 @@ import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-ic
import { prefixes } from '@/utils/constants';
interface PaginationToolsProps<TData> {
id?: string;
table: Table<TData>;
paginationOptions: number[];
onChangePaginationOption?: (newValue: number) => void;
}
function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOption }: PaginationToolsProps<TData>) {
function PaginationTools<TData>({
id,
table,
paginationOptions,
onChangePaginationOption
}: PaginationToolsProps<TData>) {
const handlePaginationOptionsChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => {
const perPage = Number(event.target.value);
@ -55,6 +61,7 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
<BiChevronLeft size='1.5rem' />
</button>
<input
id={id ? `${id}__page` : undefined}
title='Номер страницы. Выделите для ручного ввода'
className='w-6 text-center clr-app'
value={table.getState().pagination.pageIndex + 1}
@ -83,6 +90,7 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
</button>
</div>
<select
id={id ? `${id}__per_page` : undefined}
value={table.getState().pagination.pageSize}
onChange={handlePaginationOptionsChange}
className='mx-2 cursor-pointer clr-app'

View File

@ -52,14 +52,14 @@ function TableBody<TData>({
style={conditionalRowStyles && getRowStyles(row)}
>
{enableRowSelection ? (
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y align-middle'>
<td key={`select-${row.id}`} className='pl-3 pr-1 align-middle border-y'>
<SelectRow row={row} />
</td>
) : null}
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
<td
key={cell.id}
className='px-2 border-y align-middle'
className='px-2 align-middle border-y'
style={{
cursor: onRowClicked || onRowDoubleClicked ? 'pointer' : 'auto',
paddingBottom: dense ? '0.25rem' : '0.5rem',

View File

@ -147,7 +147,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
return (
<div className={clsx('flex flex-col gap-2', className, cursor)} style={style}>
<Label text={label} htmlFor={id} />
<Label text={label} />
<CodeMirror
className='font-math'
id={id}

View File

@ -182,7 +182,7 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
</AnimatePresence>
<div className={clsx('flex flex-col gap-2', cursor)}>
<Label text={label} htmlFor={id} />
<Label text={label} />
<CodeMirror
id={id}
ref={thisRef}

View File

@ -15,7 +15,6 @@ export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'>
}
function Checkbox({
id,
disabled,
label,
title,
@ -47,7 +46,6 @@ function Checkbox({
return (
<button
type='button'
id={id}
className={clsx(
'flex items-center gap-2', // prettier: split lines
'outline-none',
@ -78,9 +76,7 @@ function Checkbox({
</div>
) : null}
</div>
<label className={clsx('text-start text-sm whitespace-nowrap', cursor)} htmlFor={id}>
{label}
</label>
{label ? <span className={clsx('text-start text-sm whitespace-nowrap', cursor)}>{label}</span> : null}
</button>
);
}

View File

@ -12,7 +12,6 @@ export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'se
}
function CheckboxTristate({
id,
disabled,
label,
title,
@ -50,7 +49,6 @@ function CheckboxTristate({
return (
<button
type='button'
id={id}
className={clsx(
'flex items-center gap-2', // prettier: split lines
'outline-none',
@ -86,9 +84,7 @@ function CheckboxTristate({
</div>
) : null}
</div>
<label className={clsx('text-start text-sm whitespace-nowrap', cursor)} htmlFor={id}>
{label}
</label>
{label ? <span className={clsx('text-start text-sm whitespace-nowrap', cursor)}>{label}</span> : null}
</button>
);
}

View File

@ -15,7 +15,7 @@ interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
function FileInput({ label, acceptType, title, className, style, onChange, ...restProps }: FileInputProps) {
function FileInput({ id, label, acceptType, title, className, style, onChange, ...restProps }: FileInputProps) {
const inputRef = useRef<HTMLInputElement | null>(null);
const [fileName, setFileName] = useState('');
@ -37,6 +37,7 @@ function FileInput({ label, acceptType, title, className, style, onChange, ...re
return (
<div className={clsx('py-2', 'flex flex-col gap-2 items-center', className)} style={style}>
<input
id={id}
type='file'
ref={inputRef}
style={{ display: 'none' }}
@ -45,7 +46,7 @@ function FileInput({ label, acceptType, title, className, style, onChange, ...re
{...restProps}
/>
<Button text={label} icon={<BiUpload size='1.25rem' />} onClick={handleUploadClick} title={title} />
<Label text={fileName} />
<Label text={fileName} htmlFor={id} />
</div>
);
}

View File

@ -10,11 +10,19 @@ function Label({ text, className, ...restProps }: LabelProps) {
if (!text) {
return null;
}
if (restProps.htmlFor) {
return (
<label className={clsx('text-sm font-medium whitespace-nowrap', className)} {...restProps}>
{text}
</label>
);
} else {
return (
<span className={clsx('text-sm font-medium whitespace-nowrap', className)} {...restProps}>
{text}
</span>
);
}
}
export default Label;

View File

@ -8,7 +8,7 @@ interface LabeledValueProps {
function LabeledValue({ id, label, text, title }: LabeledValueProps) {
return (
<div className='flex justify-between gap-3'>
<label title={title}>{label}</label>
<span title={title}>{label}</span>
<span id={id}>{text}</span>
</div>
);

View File

@ -6,17 +6,19 @@ import TextInput from './TextInput';
interface SearchBarProps extends CProps.Styling {
value: string;
id?: string;
onChange?: (newValue: string) => void;
noBorder?: boolean;
}
function SearchBar({ value, onChange, noBorder, ...restProps }: SearchBarProps) {
function SearchBar({ id, value, onChange, noBorder, ...restProps }: SearchBarProps) {
return (
<div {...restProps}>
<Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'>
<BiSearchAlt2 size='1.25rem' />
</Overlay>
<TextInput
id={id}
noOutline
placeholder='Поиск'
type='search'

View File

@ -65,15 +65,26 @@ function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
onSubmit={handleSubmit}
className={clsx('px-6 py-2', classnames.flex_col)}
>
<TextInput label='Полное название' value={title} onChange={event => setTitle(event.target.value)} />
<TextInput
id='dlg_full_name'
label='Полное название'
value={title}
onChange={event => setTitle(event.target.value)}
/>
<TextInput
id='dlg_alias'
label='Сокращение'
value={alias}
className='max-w-sm'
onChange={event => setAlias(event.target.value)}
/>
<TextArea label='Комментарий' value={comment} onChange={event => setComment(event.target.value)} />
<Checkbox label='Общедоступная схема' value={common} setValue={value => setCommon(value)} />
<TextArea
id='dlg_comment'
label='Комментарий'
value={comment}
onChange={event => setComment(event.target.value)}
/>
<Checkbox id='dlg_is_common' label='Общедоступная схема' value={common} setValue={value => setCommon(value)} />
</Modal>
);
}

View File

@ -214,6 +214,7 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
</div>
<ConstituentaPicker
id='dlg_argument_picker'
value={selectedCst}
data={schema?.items}
onSelectValue={handleSelectConstituenta}

View File

@ -18,6 +18,7 @@ function ConstituentaTab({ state, partialUpdate }: ConstituentaTabProps) {
<>
<div className='flex self-center gap-3 pr-2'>
<SelectSingle
id='dlg_cst_type'
className='min-w-[14rem]'
options={SelectorCstType}
placeholder='Выберите тип'
@ -25,6 +26,7 @@ function ConstituentaTab({ state, partialUpdate }: ConstituentaTabProps) {
onChange={data => partialUpdate({ cst_type: data?.value ?? CstType.TERM })}
/>
<TextInput
id='dlg_cst_alias'
dense
label='Имя'
className='w-[7rem]'
@ -33,6 +35,7 @@ function ConstituentaTab({ state, partialUpdate }: ConstituentaTabProps) {
/>
</div>
<TextArea
id='dlg_cst_term'
spellCheck
label='Термин'
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
@ -41,6 +44,7 @@ function ConstituentaTab({ state, partialUpdate }: ConstituentaTabProps) {
onChange={event => partialUpdate({ term_raw: event.target.value })}
/>
<RSInput
id='dlg_cst_expression'
label='Формальное определение'
placeholder='Родоструктурное выражение, задающее формальное определение'
height='5.1rem'
@ -48,6 +52,7 @@ function ConstituentaTab({ state, partialUpdate }: ConstituentaTabProps) {
onChange={value => partialUpdate({ definition_formal: value })}
/>
<TextArea
id='dlg_cst_definition'
label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения'
rows={2}
@ -56,6 +61,7 @@ function ConstituentaTab({ state, partialUpdate }: ConstituentaTabProps) {
onChange={event => partialUpdate({ definition_raw: event.target.value })}
/>
<TextArea
id='dlg_cst_convention'
spellCheck
label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение к схеме'

View File

@ -105,7 +105,7 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
isClearable
/>
<SelectSingle
placeholder='Выберите источник'
placeholder='Источник'
className='w-[12rem]'
options={templateSelector}
value={
@ -117,6 +117,7 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
/>
</div>
<ConstituentaPicker
id='dlg_template_picker'
value={state.prototype}
data={filteredData}
onSelectValue={cst => partialUpdate({ prototype: cst })}
@ -124,6 +125,7 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
rows={9}
/>
<TextArea
id='dlg_template_term'
disabled
spellCheck
placeholder='Шаблон конституенты не выбран'
@ -132,6 +134,7 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
value={prototypeInfo}
/>
<RSInput
id='dlg_template_expression'
disabled
placeholder='Выберите шаблон из списка'
height='5.1rem'

View File

@ -57,6 +57,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
>
<div className='flex self-center gap-6'>
<SelectSingle
id='dlg_cst_type'
placeholder='Выберите тип'
className='min-w-[15rem]'
options={SelectorCstType}
@ -64,6 +65,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
onChange={data => updateCstData({ cst_type: data?.value ?? CstType.BASE })}
/>
<TextInput
id='dlg_cst_alias'
dense
label='Имя'
className='w-[7rem]'
@ -72,6 +74,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
/>
</div>
<TextArea
id='dlg_cst_term'
spellCheck
label='Термин'
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
@ -80,6 +83,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
onChange={event => updateCstData({ term_raw: event.target.value })}
/>
<RSInput
id='dlg_cst_expression'
label='Формальное определение'
placeholder='Родоструктурное выражение, задающее формальное определение'
height='5.1rem'
@ -87,6 +91,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
onChange={value => updateCstData({ definition_formal: value })}
/>
<TextArea
id='dlg_cst_definition'
spellCheck
label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения'
@ -95,6 +100,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
onChange={event => updateCstData({ definition_raw: event.target.value })}
/>
<TextArea
id='dlg_cst_convention'
spellCheck
label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение'

View File

@ -41,6 +41,7 @@ function DlgCreateVersion({ hideWindow, versions, onCreate }: DlgCreateVersionPr
className={clsx('w-[30rem]', 'py-2 px-6', classnames.flex_col)}
>
<TextInput
id='dlg_version'
dense
label='Версия'
className='w-[16rem]'
@ -48,6 +49,7 @@ function DlgCreateVersion({ hideWindow, versions, onCreate }: DlgCreateVersionPr
onChange={event => setVersion(event.target.value)}
/>
<TextArea
id='dlg_description'
spellCheck
label='Описание'
rows={3}

View File

@ -61,6 +61,7 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
return (
<FlexColumn>
<ConstituentaPicker
id='dlg_reference_entity_picker'
initialFilter={initial.text}
value={selectedCst}
data={items}
@ -74,6 +75,7 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
<div className='flex gap-3'>
<TextInput
id='dlg_reference_alias'
dense
label='Конституента'
placeholder='Имя'
@ -81,7 +83,16 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
value={alias}
onChange={event => setAlias(event.target.value)}
/>
<TextInput disabled dense noBorder label='Термин' className='flex-grow text-sm' value={term} title={term} />
<TextInput
id='dlg_reference_term'
disabled
dense
noBorder
label='Термин'
className='flex-grow text-sm'
value={term}
title={term}
/>
</div>
<SelectWordForm selected={selectedGrams} setSelected={setSelectedGrams} />
@ -89,6 +100,7 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
<div className='flex items-center gap-4'>
<Label text='Словоформа' />
<SelectGrammeme
id='dlg_reference_grammemes'
placeholder='Выберите граммемы'
className='flex-grow'
menuPlacement='top'

View File

@ -45,6 +45,7 @@ function SyntacticTab({ initial, setIsValid, setReference }: SyntacticTabProps)
return (
<div className='flex flex-col gap-2'>
<TextInput
id='dlg_reference_offset'
type='number'
dense
label='Смещение'
@ -53,6 +54,7 @@ function SyntacticTab({ initial, setIsValid, setReference }: SyntacticTabProps)
onChange={event => setOffset(event.target.valueAsNumber)}
/>
<TextInput
id='dlg_main_ref'
disabled // prettier: split lines
dense
noBorder
@ -60,6 +62,7 @@ function SyntacticTab({ initial, setIsValid, setReference }: SyntacticTabProps)
value={mainLink}
/>
<TextInput
id='dlg_reference_nominal'
spellCheck
label='Начальная форма'
placeholder='зависимое слово в начальной форме'

View File

@ -81,6 +81,7 @@ function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVe
/>
<div className='flex'>
<TextInput
id='dlg_version'
dense
label='Версия'
className='w-[16rem] mr-3'
@ -101,6 +102,7 @@ function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVe
/>
</div>
<TextArea
id='dlg_description'
spellCheck
label='Описание'
rows={3}

View File

@ -48,6 +48,7 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
className={clsx('w-[30rem]', 'py-6 px-6 flex gap-6 justify-center items-center')}
>
<SelectSingle
id='dlg_cst_type'
placeholder='Выберите тип'
className='min-w-[16rem] self-center'
options={SelectorCstType}
@ -58,6 +59,7 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
onChange={data => updateData({ cst_type: data?.value ?? CstType.BASE })}
/>
<TextInput
id='dlg_cst_alias'
dense
label='Имя'
className='w-[7rem]'

View File

@ -1,38 +1,51 @@
/**
* Module: API for miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
*/
import { DependencyMode } from './miscellaneous';
import { DependencyMode, ILibraryFilter, LibraryFilterStrategy } from './miscellaneous';
import { IConstituenta, IRSForm } from './rsform';
/**
* Filter list of {@link ILibraryItem} to a given query.
* Filter list of {@link ILibraryItem} to a given graph query.
*/
export function applyGraphFilter(target: IRSForm, start: number, mode: DependencyMode): IConstituenta[] {
if (mode === DependencyMode.ALL) {
return target.items;
}
let ids: number[] | undefined = undefined;
const ids: number[] | undefined = (() => {
switch (mode) {
case DependencyMode.OUTPUTS: {
ids = target.graph.nodes.get(start)?.outputs;
break;
return target.graph.nodes.get(start)?.outputs;
}
case DependencyMode.INPUTS: {
ids = target.graph.nodes.get(start)?.inputs;
break;
return target.graph.nodes.get(start)?.inputs;
}
case DependencyMode.EXPAND_OUTPUTS: {
ids = target.graph.expandOutputs([start]);
break;
return target.graph.expandOutputs([start]);
}
case DependencyMode.EXPAND_INPUTS: {
ids = target.graph.expandInputs([start]);
break;
return target.graph.expandInputs([start]);
}
}
if (!ids) {
return target.items;
return undefined;
})();
if (ids) {
return target.items.filter(cst => ids.find(id => id === cst.id));
} else {
return target.items.filter(cst => ids!.find(id => id === cst.id));
return target.items;
}
}
/**
* Filter list of {@link ILibraryItem} to a given text query.
*/
export function filterFromStrategy(strategy: LibraryFilterStrategy): ILibraryFilter {
// prettier-ignore
switch (strategy) {
case LibraryFilterStrategy.MANUAL: return {};
case LibraryFilterStrategy.COMMON: return { is_common: true };
case LibraryFilterStrategy.CANONICAL: return { is_canonical: true };
case LibraryFilterStrategy.PERSONAL: return { is_personal: true };
case LibraryFilterStrategy.SUBSCRIBE: return { is_subscribed: true };
case LibraryFilterStrategy.OWNED: return { is_owned: true };
}
}

View File

@ -85,6 +85,7 @@ function CreateRSFormPage() {
<h1>Создание концептуальной схемы</h1>
<Overlay position='top-[-2.4rem] right-[-1rem]'>
<input
id='schema_file'
ref={inputRef}
type='file'
style={{ display: 'none' }}
@ -100,6 +101,7 @@ function CreateRSFormPage() {
{fileName ? <Label text={`Загружен файл: ${fileName}`} /> : null}
<TextInput
id='schema_title'
required={!file}
label='Полное название'
placeholder={file && 'Загрузить из файла'}
@ -107,6 +109,7 @@ function CreateRSFormPage() {
onChange={event => setTitle(event.target.value)}
/>
<TextInput
id='schema_alias'
required={!file}
label='Сокращение'
placeholder={file && 'Загрузить из файла'}
@ -117,12 +120,18 @@ function CreateRSFormPage() {
onChange={event => setAlias(event.target.value)}
/>
<TextArea
id='schema_comment'
label='Комментарий'
placeholder={file && 'Загрузить из файла'}
value={comment}
onChange={event => setComment(event.target.value)}
/>
<Checkbox label='Общедоступная схема' value={common} setValue={value => setCommon(value ?? false)} />
<Checkbox
id='schema_common'
label='Общедоступная схема'
value={common}
setValue={value => setCommon(value ?? false)}
/>
<div className='flex justify-around gap-6 py-3'>
<SubmitButton text='Создать схему' loading={processing} className='min-w-[10rem]' />
<Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} />

View File

@ -11,6 +11,7 @@ import useLocalStorage from '@/hooks/useLocalStorage';
import useQueryStrings from '@/hooks/useQueryStrings';
import { ILibraryItem } from '@/models/library';
import { ILibraryFilter, LibraryFilterStrategy } from '@/models/miscellaneous';
import { filterFromStrategy } from '@/models/miscellaneousAPI';
import SearchPanel from './SearchPanel';
import ViewLibrary from './ViewLibrary';
@ -45,7 +46,7 @@ function LibraryPage() {
: LibraryFilterStrategy.MANUAL;
setQuery('');
setStrategy(inputStrategy);
setFilter(ApplyStrategy(inputStrategy));
setFilter(filterFromStrategy(inputStrategy));
}, [user, router, setQuery, setFilter, setStrategy, strategy, searchFilter]);
useLayoutEffect(() => {
@ -86,16 +87,3 @@ function LibraryPage() {
}
export default LibraryPage;
// ====== Internals =======
function ApplyStrategy(strategy: LibraryFilterStrategy): ILibraryFilter {
// prettier-ignore
switch (strategy) {
case LibraryFilterStrategy.MANUAL: return {};
case LibraryFilterStrategy.COMMON: return { is_common: true };
case LibraryFilterStrategy.CANONICAL: return { is_canonical: true };
case LibraryFilterStrategy.PERSONAL: return { is_personal: true };
case LibraryFilterStrategy.SUBSCRIBE: return { is_subscribed: true };
case LibraryFilterStrategy.OWNED: return { is_owned: true };
}
}

View File

@ -68,7 +68,13 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
</span>
</div>
<PickerStrategy value={strategy} onChange={handleChangeStrategy} />
<SearchBar noBorder className='mx-auto min-w-[10rem]' value={query} onChange={handleChangeQuery} />
<SearchBar
id='library_search'
noBorder
className='mx-auto min-w-[10rem]'
value={query}
onChange={handleChangeQuery}
/>
</div>
);
}

View File

@ -25,7 +25,7 @@ interface ViewLibraryProps {
const columnHelper = createColumnHelper<ILibraryItem>();
function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
const router = useConceptNavigation();
const intl = useIntl();
const { user } = useAuth();
@ -121,6 +121,7 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
</div>
</div>
<DataTable
id='library_data'
columns={columns}
data={items}
headPosition='2.2rem'
@ -130,7 +131,7 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
<p>Список схем пуст</p>
<p className='flex gap-6'>
<TextURL text='Создать схему' href='/library/create' />
<TextURL text='Очистить фильтр' onClick={cleanQuery} />
<TextURL text='Очистить фильтр' onClick={resetQuery} />
</p>
</FlexColumn>
}

View File

@ -68,19 +68,21 @@ function LoginPage() {
<img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
<TextInput
id='username'
label='Имя пользователя'
autoComplete='username'
autoFocus
required
allowEnter
label='Имя пользователя'
value={username}
onChange={event => setUsername(event.target.value)}
/>
<TextInput
id='password'
type='password'
label='Пароль'
autoComplete='current-password'
required
allowEnter
label='Пароль'
value={password}
onChange={event => setPassword(event.target.value)}
/>

View File

@ -82,8 +82,9 @@ function PasswordChangePage() {
<TextInput
id='new_password'
type='password'
allowEnter
label='Новый пароль'
autoComplete='new-password'
allowEnter
colors={passwordColor}
value={newPassword}
onChange={event => {
@ -93,8 +94,9 @@ function PasswordChangePage() {
<TextInput
id='new_password_repeat'
type='password'
allowEnter
label='Повторите новый'
autoComplete='new-password'
allowEnter
colors={passwordColor}
value={newPasswordRepeat}
onChange={event => {

View File

@ -127,6 +127,7 @@ function FormConstituenta({
onSubmit={handleSubmit}
>
<RefsInput
id='cst_term'
label='Термин'
placeholder='Обозначение, используемое в текстовых определениях данной схемы'
items={schema?.items}
@ -137,6 +138,7 @@ function FormConstituenta({
onChange={newValue => setTerm(newValue)}
/>
<TextArea
id='cst_typification'
dense
noBorder
disabled
@ -149,6 +151,7 @@ function FormConstituenta({
}}
/>
<EditorRSExpression
id='cst_expression'
label='Формальное определение'
placeholder='Родоструктурное выражение'
value={expression}
@ -161,6 +164,7 @@ function FormConstituenta({
setTypification={setTypification}
/>
<RefsInput
id='cst_definition'
label='Текстовое определение'
placeholder='Текстовый вариант формального определения'
height='3.8rem'
@ -172,6 +176,7 @@ function FormConstituenta({
onChange={newValue => setTextDefinition(newValue)}
/>
<TextArea
id='cst_convention'
spellCheck
label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение'

View File

@ -101,6 +101,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
onSubmit={handleSubmit}
>
<TextInput
id='schema_title'
required
label='Полное название'
value={title}
@ -109,6 +110,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
/>
<div className='flex justify-between w-full gap-3'>
<TextInput
id='schema_alias'
required
label='Сокращение'
className='w-[14rem]'
@ -149,6 +151,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
</div>
</div>
<TextArea
id='schema_comment'
label='Комментарий'
rows={3}
value={comment}
@ -157,6 +160,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
/>
<div className='flex justify-between whitespace-nowrap'>
<Checkbox
id='schema_common'
label='Общедоступная схема'
title='Общедоступные схемы видны всем пользователям и могут быть изменены'
disabled={!controller.isContentEditable}
@ -164,6 +168,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
setValue={value => setCommon(value)}
/>
<Checkbox
id='schema_immutable'
label='Неизменная схема'
title='Только администраторы могут присваивать схемам неизменный статус'
disabled={!controller.isContentEditable || !user?.is_staff}

View File

@ -75,7 +75,13 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
return (
<div className='flex border-b clr-input'>
<SearchBar noBorder className='min-w-[6rem] pr-2 flex-grow' value={filterText} onChange={setFilterText} />
<SearchBar
id='constituents_search'
noBorder
className='min-w-[6rem] pr-2 flex-grow'
value={filterText}
onChange={setFilterText}
/>
<div ref={matchModeMenu.ref}>
<SelectorButton

View File

@ -85,6 +85,7 @@ function RegisterPage() {
<TextInput
id='username'
autoComplete='username'
required
label='Имя пользователя (логин)'
pattern={patterns.login}
@ -96,6 +97,7 @@ function RegisterPage() {
<TextInput
id='password'
type='password'
autoComplete='new-password'
required
label='Пароль'
className='w-[15rem]'
@ -104,9 +106,10 @@ function RegisterPage() {
/>
<TextInput
id='password2'
required
type='password'
label='Повторите пароль'
autoComplete='new-password'
required
className='w-[15rem]'
value={password2}
onChange={event => setPassword2(event.target.value)}
@ -116,6 +119,7 @@ function RegisterPage() {
<FlexColumn className='w-[15rem]'>
<TextInput
id='email'
autoComplete='email'
required
label='Электронная почта (email)'
title='электронная почта в корректном формате, например: i.petrov@mycompany.ru.com'
@ -125,12 +129,14 @@ function RegisterPage() {
<TextInput
id='first_name'
label='Отображаемое имя'
autoComplete='given-name'
value={firstName}
onChange={event => setFirstName(event.target.value)}
/>
<TextInput
id='last_name'
label='Отображаемая фамилия'
autoComplete='family-name'
value={lastName}
onChange={event => setLastName(event.target.value)}
/>
@ -138,7 +144,7 @@ function RegisterPage() {
</div>
<div className='flex gap-1 text-sm'>
<Checkbox label='Принимаю условия' value={acceptPrivacy} setValue={setAcceptPrivacy} />
<Checkbox id='accept_terms' label='Принимаю условия' value={acceptPrivacy} setValue={setAcceptPrivacy} />
<TextURL text='обработки персональных данных...' href={'/manuals?topic=privacy'} />
</div>

View File

@ -70,16 +70,18 @@ function EditorPassword() {
<TextInput
id='old_password'
type='password'
allowEnter
label='Старый пароль'
autoComplete='current-password'
allowEnter
value={oldPassword}
onChange={event => setOldPassword(event.target.value)}
/>
<TextInput
id='new_password'
type='password'
allowEnter
label='Новый пароль'
autoComplete='new-password'
allowEnter
colors={passwordColor}
value={newPassword}
onChange={event => {
@ -89,8 +91,9 @@ function EditorPassword() {
<TextInput
id='new_password_repeat'
type='password'
allowEnter
label='Повторите новый'
autoComplete='new-password'
allowEnter
colors={passwordColor}
value={newPasswordRepeat}
onChange={event => {

View File

@ -49,9 +49,17 @@ function EditorProfile() {
return (
<form onSubmit={handleSubmit} className={clsx('min-w-[18rem]', 'px-6 py-2', classnames.flex_col)}>
<TextInput id='username' disabled label='Логин' title='Логин изменить нельзя' value={username} />
<TextInput
id='username'
autoComplete='username'
disabled
label='Логин'
title='Логин изменить нельзя'
value={username}
/>
<TextInput
id='first_name'
autoComplete='off'
allowEnter
label='Имя'
value={first_name}
@ -59,6 +67,7 @@ function EditorProfile() {
/>
<TextInput
id='last_name'
autoComplete='off'
allowEnter
label='Фамилия'
value={last_name}
@ -66,6 +75,7 @@ function EditorProfile() {
/>
<TextInput
id='email'
autoComplete='off'
allowEnter
label='Электронная почта'
value={email}

View File

@ -88,33 +88,33 @@ export const urls = {
* Global unique IDs.
*/
export const globalIDs = {
tooltip: 'global-tooltip',
password_tooltip: 'password-tooltip',
main_scroll: 'main-scroll',
library_item_editor: 'library-item-editor',
constituenta_editor: 'constituenta-editor'
tooltip: 'global_tooltip',
password_tooltip: 'password_tooltip',
main_scroll: 'main_scroll',
library_item_editor: 'library_item_editor',
constituenta_editor: 'constituenta_editor'
};
/**
* Prefixes for generating unique keys for lists.
*/
export const prefixes = {
page_size: 'page-size-',
cst_list: 'cst-list-',
cst_side_table: 'cst-side-table-',
cst_hidden_list: 'cst-hidden-list-',
cst_modal_list: 'cst-modal-list-',
cst_template_ist: 'cst-template-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-',
cst_delete_list: 'cst-delete-list-',
cst_dependant_list: 'cst-dependant-list-',
csttype_list: 'csttype-',
library_filters_list: 'library-filters-list-',
topic_list: 'topic-list-',
library_list: 'library-list-',
wordform_list: 'wordform-list-',
rsedit_btn: 'rsedit-btn-'
page_size: 'page_size_',
cst_list: 'cst_list_',
cst_side_table: 'cst_side_table_',
cst_hidden_list: 'cst_hidden_list_',
cst_modal_list: 'cst_modal_list_',
cst_template_ist: 'cst_template_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_',
cst_delete_list: 'cst_delete_list_',
cst_dependant_list: 'cst_dependant_list_',
csttype_list: 'csttype_',
library_filters_list: 'library_filters_list_',
topic_list: 'topic_list_',
library_list: 'library_list_',
wordform_list: 'wordform_list_',
rsedit_btn: 'rsedit_btn_'
};