M: Improve location picker

This commit is contained in:
Ivan 2025-03-11 22:24:51 +03:00
parent c04ea8993e
commit 92d3d2676b
6 changed files with 95 additions and 132 deletions

View File

@ -0,0 +1,65 @@
'use client';
import { type FieldError } from 'react-hook-form';
import clsx from 'clsx';
import { useAuthSuspense } from '@/features/auth';
import { Label, TextArea } from '@/components/Input';
import { type Styling } from '@/components/props';
import { LocationHead } from '../../models/library';
import { combineLocation } from '../../models/libraryAPI';
import { SelectLocationHead } from '../SelectLocationHead';
import { SelectLocationContext } from './SelectLocationContext';
interface PickLocationProps extends Styling {
dropdownHeight?: string;
rows?: number;
value: string;
onChange: (newLocation: string) => void;
error?: FieldError;
}
export function PickLocation({
dropdownHeight,
rows = 3,
value,
onChange,
error,
className,
...restProps
}: PickLocationProps) {
const { user } = useAuthSuspense();
return (
<div className={clsx('flex', className)} {...restProps}>
<div className='flex flex-col gap-2 min-w-28'>
<Label className='select-none' text='Корень' />
<SelectLocationHead
value={value.substring(0, 2) as LocationHead}
onChange={newValue => onChange(combineLocation(newValue, value.substring(3)))}
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []}
/>
</div>
<SelectLocationContext
className='-mt-1 -ml-8'
dropdownHeight={dropdownHeight} //
value={value}
onChange={onChange}
/>
<TextArea
id='dlg_location'
label='Путь'
rows={rows}
value={value.substring(3)}
onChange={event => onChange(combineLocation(value.substring(0, 2), event.target.value))}
error={error}
/>
</div>
);
}

View File

@ -8,7 +8,7 @@ import { IconFolderTree } from '@/components/Icons';
import { type Styling } from '@/components/props'; import { type Styling } from '@/components/props';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { SelectLocation } from './SelectLocation'; import { SelectLocation } from '../SelectLocation';
interface SelectLocationContextProps extends Styling { interface SelectLocationContextProps extends Styling {
value: string; value: string;
@ -22,7 +22,7 @@ export function SelectLocationContext({
title = 'Проводник...', title = 'Проводник...',
onChange, onChange,
className, className,
dropdownHeight, dropdownHeight = 'h-50',
...restProps ...restProps
}: SelectLocationContextProps) { }: SelectLocationContextProps) {
const menu = useDropdown(); const menu = useDropdown();
@ -35,14 +35,14 @@ export function SelectLocationContext({
} }
return ( return (
<div ref={menu.ref} className={clsx('relative h-full -mt-1 -ml-6 text-right self-start', className)} {...restProps}> <div ref={menu.ref} className={clsx('relative text-right self-start', className)} {...restProps}>
<MiniButton <MiniButton
title={title} title={title}
hideTitle={menu.isOpen} hideTitle={menu.isOpen}
icon={<IconFolderTree size='1.25rem' className='icon-green' />} icon={<IconFolderTree size='1.25rem' className='icon-green' />}
onClick={() => menu.toggle()} onClick={() => menu.toggle()}
/> />
<Dropdown isOpen={menu.isOpen} className={clsx('w-80 h-50 z-tooltip', dropdownHeight)}> <Dropdown isOpen={menu.isOpen} className={clsx('w-80 z-tooltip', dropdownHeight)}>
<SelectLocation <SelectLocation
value={value} value={value}
prefix={prefixes.folders_list} prefix={prefixes.folders_list}

View File

@ -0,0 +1 @@
export { PickLocation } from './PickLocation';

View File

@ -4,18 +4,13 @@ import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
import { useAuthSuspense } from '@/features/auth';
import { Label, TextArea } from '@/components/Input';
import { ModalForm } from '@/components/Modal'; import { ModalForm } from '@/components/Modal';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { limits } from '@/utils/constants'; import { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
import { SelectLocationContext } from '../components/SelectLocationContext'; import { PickLocation } from '../components/PickLocation';
import { SelectLocationHead } from '../components/SelectLocationHead'; import { validateLocation } from '../models/libraryAPI';
import { LocationHead } from '../models/library';
import { combineLocation, validateLocation } from '../models/libraryAPI';
const schemaLocation = z.strictObject({ const schemaLocation = z.strictObject({
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }) location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation })
@ -30,7 +25,6 @@ export interface DlgChangeLocationProps {
export function DlgChangeLocation() { export function DlgChangeLocation() {
const { initial, onChangeLocation } = useDialogsStore(state => state.props as DlgChangeLocationProps); const { initial, onChangeLocation } = useDialogsStore(state => state.props as DlgChangeLocationProps);
const { user } = useAuthSuspense();
const { const {
handleSubmit, handleSubmit,
@ -56,39 +50,16 @@ export function DlgChangeLocation() {
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`} submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`}
canSubmit={isValid && isDirty} canSubmit={isValid && isDirty}
onSubmit={event => void handleSubmit(onSubmit)(event)} onSubmit={event => void handleSubmit(onSubmit)(event)}
className='w-140 pb-3 px-6 flex gap-3 h-36' className='w-130 pb-3 px-6 h-36'
> >
<div className='flex flex-col gap-2 min-w-28'>
<Label className='select-none' text='Корень' />
<Controller <Controller
control={control} control={control}
name='location' name='location'
render={({ field }) => ( render={({ field }) => (
<SelectLocationHead <PickLocation
value={field.value.substring(0, 2) as LocationHead} dropdownHeight='h-38' //
onChange={newValue => field.onChange(combineLocation(newValue, field.value.substring(3)))} value={field.value}
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []} onChange={field.onChange}
/>
)}
/>
</div>
<Controller
control={control}
name='location'
render={({ field }) => (
<SelectLocationContext dropdownHeight='max-h-36' value={field.value} onChange={field.onChange} />
)}
/>
<Controller
control={control}
name='location'
render={({ field }) => (
<TextArea
id='dlg_location'
label='Путь'
rows={3}
value={field.value.substring(3)}
onChange={event => field.onChange(combineLocation(field.value.substring(0, 2), event.target.value))}
error={errors.location} error={errors.location}
/> />
)} )}

View File

@ -4,7 +4,6 @@ import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { Checkbox, Label, TextArea, TextInput } from '@/components/Input'; import { Checkbox, Label, TextArea, TextInput } from '@/components/Input';
@ -14,11 +13,9 @@ import { useDialogsStore } from '@/stores/dialogs';
import { AccessPolicy, type ICloneLibraryItemDTO, type ILibraryItem, schemaCloneLibraryItem } from '../backend/types'; import { AccessPolicy, type ICloneLibraryItemDTO, type ILibraryItem, schemaCloneLibraryItem } from '../backend/types';
import { useCloneItem } from '../backend/useCloneItem'; import { useCloneItem } from '../backend/useCloneItem';
import { IconItemVisibility } from '../components/IconItemVisibility'; import { IconItemVisibility } from '../components/IconItemVisibility';
import { PickLocation } from '../components/PickLocation';
import { SelectAccessPolicy } from '../components/SelectAccessPolicy'; import { SelectAccessPolicy } from '../components/SelectAccessPolicy';
import { SelectLocationContext } from '../components/SelectLocationContext'; import { cloneTitle } from '../models/libraryAPI';
import { SelectLocationHead } from '../components/SelectLocationHead';
import { LocationHead } from '../models/library';
import { cloneTitle, combineLocation } from '../models/libraryAPI';
export interface DlgCloneLibraryItemProps { export interface DlgCloneLibraryItemProps {
base: ILibraryItem; base: ILibraryItem;
@ -32,7 +29,6 @@ export function DlgCloneLibraryItem() {
state => state.props as DlgCloneLibraryItemProps state => state.props as DlgCloneLibraryItemProps
); );
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuthSuspense();
const { cloneItem } = useCloneItem(); const { cloneItem } = useCloneItem();
const { const {
@ -108,46 +104,13 @@ export function DlgCloneLibraryItem() {
</div> </div>
</div> </div>
<div className='flex gap-3'>
<div className='flex flex-col gap-2 w-28'>
<Label text='Корень' />
<Controller <Controller
control={control} // control={control}
name='location' name='location'
render={({ field }) => ( render={({ field }) => (
<SelectLocationHead <PickLocation value={field.value} rows={2} onChange={field.onChange} error={errors.location} />
value={field.value.substring(0, 2) as LocationHead}
onChange={newValue => field.onChange(combineLocation(newValue, field.value.substring(3)))}
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []}
/>
)} )}
/> />
</div>
<Controller
control={control} //
name='location'
render={({ field }) => (
<SelectLocationContext
value={field.value} //
onChange={field.onChange}
/>
)}
/>
<Controller
control={control} //
name='location'
render={({ field }) => (
<TextArea
id='dlg_location'
label='Путь'
rows={3}
value={field.value.substring(3)}
onChange={event => field.onChange(combineLocation(field.value.substring(0, 2), event.target.value))}
error={errors.location}
/>
)}
/>
</div>
<TextArea id='dlg_comment' {...register('comment')} label='Описание' rows={4} error={errors.comment} /> <TextArea id='dlg_comment' {...register('comment')} label='Описание' rows={4} error={errors.comment} />

View File

@ -5,7 +5,6 @@ import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { Button, MiniButton, SubmitButton } from '@/components/Control'; import { Button, MiniButton, SubmitButton } from '@/components/Control';
import { IconDownload } from '@/components/Icons'; import { IconDownload } from '@/components/Icons';
@ -21,17 +20,14 @@ import {
} from '../../backend/types'; } from '../../backend/types';
import { useCreateItem } from '../../backend/useCreateItem'; import { useCreateItem } from '../../backend/useCreateItem';
import { IconItemVisibility } from '../../components/IconItemVisibility'; import { IconItemVisibility } from '../../components/IconItemVisibility';
import { PickLocation } from '../../components/PickLocation';
import { SelectAccessPolicy } from '../../components/SelectAccessPolicy'; import { SelectAccessPolicy } from '../../components/SelectAccessPolicy';
import { SelectItemType } from '../../components/SelectItemType'; import { SelectItemType } from '../../components/SelectItemType';
import { SelectLocationContext } from '../../components/SelectLocationContext';
import { SelectLocationHead } from '../../components/SelectLocationHead';
import { LocationHead } from '../../models/library'; import { LocationHead } from '../../models/library';
import { combineLocation } from '../../models/libraryAPI';
import { useLibrarySearchStore } from '../../stores/librarySearch'; import { useLibrarySearchStore } from '../../stores/librarySearch';
export function FormCreateItem() { export function FormCreateItem() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuthSuspense();
const { createItem, isPending, error: serverError, reset: clearServerError } = useCreateItem(); const { createItem, isPending, error: serverError, reset: clearServerError } = useCreateItem();
const searchLocation = useLibrarySearchStore(state => state.location); const searchLocation = useLibrarySearchStore(state => state.location);
@ -196,6 +192,14 @@ export function FormCreateItem() {
</div> </div>
</div> </div>
<Controller
control={control}
name='location'
render={({ field }) => (
<PickLocation value={field.value} rows={2} onChange={field.onChange} error={errors.location} />
)}
/>
<TextArea <TextArea
id='schema_comment' id='schema_comment'
{...register('comment')} {...register('comment')}
@ -204,47 +208,6 @@ export function FormCreateItem() {
error={errors.comment} error={errors.comment}
/> />
<div className='flex justify-between gap-3 grow'>
<div className='flex flex-col gap-2 min-w-28'>
<Label text='Корень' />
<Controller
control={control} //
name='location'
render={({ field }) => (
<SelectLocationHead
value={field.value.substring(0, 2) as LocationHead}
onChange={newValue => field.onChange(combineLocation(newValue, field.value.substring(3)))}
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []}
/>
)}
/>
</div>
<Controller
control={control} //
name='location'
render={({ field }) => (
<SelectLocationContext
value={field.value} //
onChange={field.onChange}
/>
)}
/>
<Controller
control={control} //
name='location'
render={({ field }) => (
<TextArea
id='location'
label='Путь'
rows={4}
value={field.value.substring(3)}
onChange={event => field.onChange(combineLocation(field.value.substring(0, 2), event.target.value))}
error={errors.location}
/>
)}
/>
</div>
<div className='flex justify-around gap-6 py-3'> <div className='flex justify-around gap-6 py-3'>
<SubmitButton text='Создать схему' loading={isPending} className='min-w-40' /> <SubmitButton text='Создать схему' loading={isPending} className='min-w-40' />
<Button text='Отмена' className='min-w-40' onClick={() => handleCancel()} /> <Button text='Отмена' className='min-w-40' onClick={() => handleCancel()} />