mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-14 12:50:37 +03:00
R: Refactor components to isolate reusable parts
This commit is contained in:
parent
6b69544b8b
commit
ca6249afe0
|
@ -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>
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
|
@ -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>`}
|
|
@ -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'>
|
|
@ -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'>
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user