R: Refactor components to isolate reusable parts

This commit is contained in:
Ivan 2025-07-02 12:14:46 +03:00
parent cb0f8783e5
commit f365b20814
13 changed files with 126 additions and 113 deletions

View File

@ -9,24 +9,16 @@ import {
import { cn } from '@/components/utils';
import { ValueStats } from '@/components/view';
import { type IOperationSchemaStats } from '../../../models/oss';
import { type IOperationSchemaStats } from '../models/oss';
interface OssStatsProps {
className?: string;
isMounted: boolean;
stats: IOperationSchemaStats;
}
export function OssStats({ className, isMounted, stats }: OssStatsProps) {
export function OssStats({ className, stats }: OssStatsProps) {
return (
<aside
className={cn(
'grid grid-cols-4 gap-1 justify-items-end h-min',
'cc-animate-sidebar',
isMounted ? 'max-w-full' : 'opacity-0 max-w-0',
className
)}
>
<aside className={cn('grid grid-cols-4 gap-1 justify-items-end h-min', className)}>
<div id='count_operations' className='w-fit flex gap-3 hover:cursor-default '>
<span>Всего</span>
<span>{stats.count_all}</span>

View File

@ -9,10 +9,10 @@ import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
import { globalIDs } from '@/utils/constants';
import { OssStats } from '../../../components/oss-stats';
import { useOssEdit } from '../oss-edit-context';
import { FormOSS } from './form-oss';
import { OssStats } from './oss-stats';
const SIDELIST_LAYOUT_THRESHOLD = 768; // px
@ -63,9 +63,12 @@ export function EditorOssCard() {
</div>
<OssStats
className='w-80 md:w-56 mt-3 md:mt-8 mx-auto md:ml-5 md:mr-0'
className={clsx(
'w-80 md:w-56 mt-3 md:mt-8 mx-auto md:ml-5 md:mr-0',
'cc-animate-sidebar',
showOSSStats ? 'max-w-full' : 'opacity-0 max-w-0'
)}
stats={schema.stats}
isMounted={showOSSStats}
/>
</div>
);

View File

@ -20,26 +20,16 @@ import {
import { cn } from '@/components/utils';
import { ValueStats } from '@/components/view';
import { type IRSFormStats } from '../../../models/rsform';
import { type IRSFormStats } from '../models/rsform';
interface RSFormStatsProps {
className?: string;
isArchive: boolean;
isMounted: boolean;
stats: IRSFormStats;
}
export function RSFormStats({ className, stats, isArchive, isMounted }: RSFormStatsProps) {
export function RSFormStats({ className, stats }: RSFormStatsProps) {
return (
<aside
className={cn(
'cc-animate-sidebar',
'h-min',
'grid grid-cols-4 gap-1 justify-items-end ',
isMounted ? 'max-w-full' : 'opacity-0 max-w-0',
className
)}
>
<aside className={cn('h-min', 'grid grid-cols-4 gap-1 justify-items-end ', className)}>
<div id='count_all' className='col-span-2 w-fit flex gap-3 hover:cursor-default'>
<span>Всего</span>
<span>{stats.count_all}</span>
@ -54,7 +44,7 @@ export function RSFormStats({ className, stats, isArchive, isMounted }: RSFormSt
id='count_inherited'
icon={<IconChild size='1.25rem' />}
value={stats.count_inherited}
titleHtml={isArchive ? 'Архивные схемы не хранят<br/> информацию о наследовании' : 'Наследованные'}
titleHtml='Наследованные'
/>
<ValueStats

View File

@ -4,17 +4,19 @@ import { MiniButton } from '@/components/control';
import { IconChild } from '@/components/icons';
import { SearchBar } from '@/components/input';
import { useCstSearchStore } from '../../../stores/cst-search';
import { useRSEdit } from '../rsedit-context';
import { type IRSForm } from '../../models/rsform';
import { useCstSearchStore } from '../../stores/cst-search';
import { SelectGraphFilter } from './select-graph-filter';
import { SelectMatchMode } from './select-match-mode';
interface ConstituentsSearchProps {
schema: IRSForm;
dense?: boolean;
hideGraphFilter?: boolean;
}
export function ConstituentsSearch({ dense }: ConstituentsSearchProps) {
export function ConstituentsSearch({ schema, dense, hideGraphFilter }: ConstituentsSearchProps) {
const query = useCstSearchStore(state => state.query);
const filterMatch = useCstSearchStore(state => state.match);
const filterSource = useCstSearchStore(state => state.source);
@ -24,8 +26,6 @@ export function ConstituentsSearch({ dense }: ConstituentsSearchProps) {
const setSource = useCstSearchStore(state => state.setSource);
const toggleInherited = useCstSearchStore(state => state.toggleInherited);
const schema = useRSEdit().schema;
return (
<div className='flex border-b bg-input rounded-t-md'>
<SearchBar
@ -36,7 +36,7 @@ export function ConstituentsSearch({ dense }: ConstituentsSearchProps) {
onChangeQuery={setQuery}
/>
<SelectMatchMode value={filterMatch} onChange={setMatch} dense={dense} />
<SelectGraphFilter value={filterSource} onChange={setSource} dense={dense} />
{!hideGraphFilter ? <SelectGraphFilter value={filterSource} onChange={setSource} dense={dense} /> : null}
{schema.stats.count_inherited > 0 ? (
<MiniButton
titleHtml={`Наследованные: <b>${includeInherited ? 'отображать' : 'скрывать'}</b>`}

View File

@ -4,12 +4,11 @@ import { SelectorButton } from '@/components/control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
import { type Styling } from '@/components/props';
import { cn } from '@/components/utils';
import { useWindowSize } from '@/hooks/use-window-size';
import { prefixes } from '@/utils/constants';
import { IconDependencyMode } from '../../../components/icon-dependency-mode';
import { describeCstSource, labelCstSource } from '../../../labels';
import { DependencyMode } from '../../../stores/cst-search';
import { describeCstSource, labelCstSource } from '../../labels';
import { DependencyMode } from '../../stores/cst-search';
import { IconDependencyMode } from '../icon-dependency-mode';
interface SelectGraphFilterProps extends Styling {
value: DependencyMode;
@ -19,7 +18,6 @@ interface SelectGraphFilterProps extends Styling {
export function SelectGraphFilter({ value, dense, className, onChange, ...restProps }: SelectGraphFilterProps) {
const menu = useDropdown();
const size = useWindowSize();
function handleChange(newValue: DependencyMode) {
menu.hide();
@ -34,7 +32,7 @@ export function SelectGraphFilter({ value, dense, className, onChange, ...restPr
hideTitle={menu.isOpen}
className='h-full pr-2'
icon={<IconDependencyMode value={value} size='1rem' />}
text={!dense && !size.isSmall ? labelCstSource(value) : undefined}
text={!dense ? labelCstSource(value) : undefined}
onClick={menu.toggle}
/>
<Dropdown stretchLeft isOpen={menu.isOpen} margin='mt-3'>

View File

@ -4,12 +4,11 @@ import { SelectorButton } from '@/components/control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
import { type Styling } from '@/components/props';
import { cn } from '@/components/utils';
import { useWindowSize } from '@/hooks/use-window-size';
import { prefixes } from '@/utils/constants';
import { IconCstMatchMode } from '../../../components/icon-cst-match-mode';
import { describeCstMatchMode, labelCstMatchMode } from '../../../labels';
import { CstMatchMode } from '../../../stores/cst-search';
import { describeCstMatchMode, labelCstMatchMode } from '../../labels';
import { CstMatchMode } from '../../stores/cst-search';
import { IconCstMatchMode } from '../icon-cst-match-mode';
interface SelectMatchModeProps extends Styling {
value: CstMatchMode;
@ -19,7 +18,6 @@ interface SelectMatchModeProps extends Styling {
export function SelectMatchMode({ value, dense, className, onChange, ...restProps }: SelectMatchModeProps) {
const menu = useDropdown();
const size = useWindowSize();
function handleChange(newValue: CstMatchMode) {
menu.hide();
@ -33,7 +31,7 @@ export function SelectMatchMode({ value, dense, className, onChange, ...restProp
hideTitle={menu.isOpen}
className='h-full pr-2'
icon={<IconCstMatchMode value={value} size='1rem' />}
text={dense || size.isSmall ? undefined : labelCstMatchMode(value)}
text={!dense ? labelCstMatchMode(value) : undefined}
onClick={menu.toggle}
/>
<Dropdown stretchLeft isOpen={menu.isOpen} margin='mt-3'>

View File

@ -6,25 +6,33 @@ import { createColumnHelper, DataTable, type IConditionalStyle } from '@/compone
import { NoData, TextContent } from '@/components/view';
import { PARAMETER, prefixes } from '@/utils/constants';
import { BadgeConstituenta } from '../../../components/badge-constituenta';
import { describeConstituenta } from '../../../labels';
import { type IConstituenta } from '../../../models/rsform';
import { useRSEdit } from '../rsedit-context';
import { describeConstituenta } from '../../labels';
import { type IConstituenta, type IRSForm } from '../../models/rsform';
import { BadgeConstituenta } from '../badge-constituenta';
import { useFilteredItems } from './use-filtered-items';
const DESCRIPTION_MAX_SYMBOLS = 280;
interface TableSideConstituentsProps {
schema: IRSForm;
activeCst?: IConstituenta | null;
onActivate?: (cst: IConstituenta) => void;
maxHeight?: string;
autoScroll?: boolean;
maxHeight: string;
}
const columnHelper = createColumnHelper<IConstituenta>();
export function TableSideConstituents({ autoScroll = true, maxHeight }: TableSideConstituentsProps) {
const { activeCst, navigateCst } = useRSEdit();
const items = useFilteredItems();
export function TableSideConstituents({
schema,
activeCst,
onActivate,
maxHeight,
autoScroll = true
}: TableSideConstituentsProps) {
const items = useFilteredItems(schema, activeCst);
const prevActiveCstID = useRef<number | null>(null);
if (autoScroll && prevActiveCstID.current !== activeCst?.id) {
@ -81,7 +89,7 @@ export function TableSideConstituents({ autoScroll = true, maxHeight }: TableSid
dense
noFooter
className='text-sm select-none cc-scroll-y'
style={{ maxHeight: maxHeight }}
style={maxHeight ? { maxHeight: maxHeight } : {}}
data={items}
columns={columns}
conditionalRowStyles={conditionalRowStyles}
@ -93,7 +101,7 @@ export function TableSideConstituents({ autoScroll = true, maxHeight }: TableSid
<p>Измените параметры фильтра</p>
</NoData>
}
onRowClicked={cst => navigateCst(cst.id)}
onRowClicked={onActivate ? cst => onActivate(cst) : undefined}
/>
);
}

View File

@ -1,11 +1,8 @@
import { type IConstituenta, type IRSForm } from '../../../models/rsform';
import { matchConstituenta } from '../../../models/rsform-api';
import { DependencyMode, useCstSearchStore } from '../../../stores/cst-search';
import { useRSEdit } from '../rsedit-context';
export function useFilteredItems() {
const { schema, activeCst } = useRSEdit();
import { type IConstituenta, type IRSForm } from '../../models/rsform';
import { matchConstituenta } from '../../models/rsform-api';
import { DependencyMode, useCstSearchStore } from '../../stores/cst-search';
export function useFilteredItems(schema: IRSForm, activeCst?: IConstituenta | null): IConstituenta[] {
const query = useCstSearchStore(state => state.query);
const filterMatch = useCstSearchStore(state => state.match);
const filterSource = useCstSearchStore(state => state.source);

View File

@ -0,0 +1,49 @@
'use client';
import { type IConstituenta, type IRSForm } from '@/features/rsform/models/rsform';
import { cn } from '@/components/utils';
import { ConstituentsSearch } from './constituents-search';
import { TableSideConstituents } from './table-side-constituents';
interface ViewConstituentsProps {
schema: IRSForm;
activeCst?: IConstituenta | null;
onActivate?: (cst: IConstituenta) => void;
className?: string;
maxListHeight?: string;
noBorder?: boolean;
dense?: boolean;
autoScroll?: boolean;
}
export function ViewConstituents({
schema,
activeCst,
onActivate,
className,
maxListHeight,
dense,
noBorder,
autoScroll
}: ViewConstituentsProps) {
return (
<aside className={cn(!noBorder && 'border', className)}>
<ConstituentsSearch
schema={schema} //
dense={dense}
hideGraphFilter={!activeCst}
/>
<TableSideConstituents
schema={schema}
activeCst={activeCst}
onActivate={onActivate}
maxHeight={maxListHeight}
autoScroll={autoScroll}
/>
</aside>
);
}

View File

@ -3,15 +3,17 @@
import { useEffect, useState } from 'react';
import clsx from 'clsx';
import { useRoleStore, UserRole } from '@/features/users';
import { useWindowSize } from '@/hooks/use-window-size';
import { useMainHeight } from '@/stores/app-layout';
import { useFitHeight, useMainHeight } from '@/stores/app-layout';
import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
import { globalIDs } from '@/utils/constants';
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
import { ViewConstituents } from '../../../components/view-constituents';
import { useRSEdit } from '../rsedit-context';
import { ViewConstituents } from '../view-constituents';
import { FormConstituenta } from './form-constituenta';
import { ToolbarConstituenta } from './toolbar-constituenta';
@ -19,6 +21,9 @@ import { ToolbarConstituenta } from './toolbar-constituenta';
// Threshold window width to switch layout.
const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
// Window width cutoff for dense search bar
const COLUMN_DENSE_SEARCH_THRESHOLD = 1100;
export function EditorConstituenta() {
const { schema, activeCst, isContentEditable, selected, setSelected, moveUp, moveDown, cloneCst, navigateCst } =
useRSEdit();
@ -34,6 +39,9 @@ export function EditorConstituenta() {
const disabled = !activeCst || !isContentEditable || isProcessing;
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
const role = useRoleStore(state => state.role);
const listHeight = useFitHeight(!isNarrow ? '8.2rem' : role !== UserRole.READER ? '42rem' : '35rem', '10rem');
useEffect(() => {
if (activeCst && selected.length !== 1) {
setSelected([activeCst.id]);
@ -113,9 +121,17 @@ export function EditorConstituenta() {
) : null}
</div>
<ViewConstituents
className={isNarrow ? 'mt-3 mx-6 overflow-hidden' : 'mt-9 overflow-visible'}
isMounted={showList}
isBottom={isNarrow}
className={clsx(
'cc-animate-sidebar',
isNarrow ? 'mt-3 mx-6 rounded-md overflow-hidden' : 'mt-9 rounded-l-md rounded-r-none overflow-visible',
showList ? 'max-w-full' : 'opacity-0 max-w-0'
)}
schema={schema}
activeCst={activeCst}
onActivate={cst => navigateCst(cst.id)}
dense={!!windowSize.width && windowSize.width < COLUMN_DENSE_SEARCH_THRESHOLD}
maxListHeight={listHeight}
autoScroll={!isNarrow}
/>
</div>
);

View File

@ -9,15 +9,15 @@ import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
import { globalIDs } from '@/utils/constants';
import { RSFormStats } from '../../../components/rsform-stats';
import { useRSEdit } from '../rsedit-context';
import { FormRSForm } from './form-rsform';
import { RSFormStats } from './rsform-stats';
const SIDELIST_LAYOUT_THRESHOLD = 768; // px
export function EditorRSFormCard() {
const { schema, isArchive, isMutable, deleteSchema, isAttachedToOSS } = useRSEdit();
const { schema, isMutable, deleteSchema, isAttachedToOSS } = useRSEdit();
const { isModified } = useModificationStore();
const showRSFormStats = usePreferencesStore(state => state.showRSFormStats);
const windowSize = useWindowSize();
@ -63,10 +63,12 @@ export function EditorRSFormCard() {
</div>
<RSFormStats
className='w-80 md:w-56 mt-3 md:mt-8 mx-auto md:ml-5 md:mr-0'
className={clsx(
'w-80 md:w-56 mt-3 md:mt-8 mx-auto md:ml-5 md:mr-0',
'cc-animate-sidebar',
showRSFormStats ? 'max-w-full' : 'opacity-0 max-w-0'
)}
stats={schema.stats}
isArchive={isArchive}
isMounted={showRSFormStats}
/>
</div>
);

View File

@ -1,40 +0,0 @@
'use client';
import { useRoleStore, UserRole } from '@/features/users';
import { cn } from '@/components/utils';
import { useWindowSize } from '@/hooks/use-window-size';
import { useFitHeight } from '@/stores/app-layout';
import { ConstituentsSearch } from './constituents-search';
import { TableSideConstituents } from './table-side-constituents';
// Window width cutoff for dense search bar
const COLUMN_DENSE_SEARCH_THRESHOLD = 1100;
interface ViewConstituentsProps {
className?: string;
isBottom?: boolean;
isMounted: boolean;
}
export function ViewConstituents({ className, isBottom, isMounted }: ViewConstituentsProps) {
const windowSize = useWindowSize();
const role = useRoleStore(state => state.role);
const listHeight = useFitHeight(!isBottom ? '8.2rem' : role !== UserRole.READER ? '42rem' : '35rem', '10rem');
return (
<aside
className={cn(
'cc-animate-sidebar',
'border',
isBottom ? 'rounded-md' : 'rounded-l-md rounded-r-none h-fit',
isMounted ? 'max-w-full' : 'opacity-0 max-w-0',
className
)}
>
<ConstituentsSearch dense={!!windowSize.width && windowSize.width < COLUMN_DENSE_SEARCH_THRESHOLD} />
<TableSideConstituents maxHeight={listHeight} autoScroll={!isBottom} />
</aside>
);
}