M: Improve location picker
This commit is contained in:
parent
c04ea8993e
commit
92d3d2676b
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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}
|
|
@ -0,0 +1 @@
|
||||||
|
export { PickLocation } from './PickLocation';
|
|
@ -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
|
|
||||||
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='location'
|
name='location'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<SelectLocationContext dropdownHeight='max-h-36' value={field.value} onChange={field.onChange} />
|
<PickLocation
|
||||||
)}
|
dropdownHeight='h-38' //
|
||||||
/>
|
value={field.value}
|
||||||
<Controller
|
onChange={field.onChange}
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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'>
|
<Controller
|
||||||
<div className='flex flex-col gap-2 w-28'>
|
control={control}
|
||||||
<Label text='Корень' />
|
name='location'
|
||||||
<Controller
|
render={({ field }) => (
|
||||||
control={control} //
|
<PickLocation value={field.value} rows={2} onChange={field.onChange} error={errors.location} />
|
||||||
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='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} />
|
||||||
|
|
||||||
|
|
|
@ -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()} />
|
||||||
|
|
Loading…
Reference in New Issue
Block a user