F: Rework version editing

This commit is contained in:
Ivan 2025-02-07 15:21:40 +03:00
parent 0320dd68ab
commit 6187146e86
8 changed files with 101 additions and 82 deletions

View File

@ -5,15 +5,7 @@ import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTrans
import { DELAYS } from '@/backend/configuration'; import { DELAYS } from '@/backend/configuration';
import { ossApi } from '@/backend/oss/api'; import { ossApi } from '@/backend/oss/api';
import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api'; import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api';
import { import { AccessPolicy, ILibraryItem, IVersionInfo, LibraryItemID, LibraryItemType, VersionID } from '@/models/library';
AccessPolicy,
ILibraryItem,
IVersionData,
IVersionInfo,
LibraryItemID,
LibraryItemType,
VersionID
} from '@/models/library';
import { validateLocation } from '@/models/libraryAPI'; import { validateLocation } from '@/models/libraryAPI';
import { ConstituentaID } from '@/models/rsform'; import { ConstituentaID } from '@/models/rsform';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
@ -90,7 +82,7 @@ export type IUpdateLibraryItemDTO = z.infer<typeof UpdateLibraryItemSchema>;
/** /**
* Create version metadata in persistent storage. * Create version metadata in persistent storage.
*/ */
export const CreateVersionSchema = z.object({ export const VersionCreateSchema = z.object({
version: z.string(), version: z.string(),
description: z.string(), description: z.string(),
items: z.array(z.number()).optional() items: z.array(z.number()).optional()
@ -99,7 +91,7 @@ export const CreateVersionSchema = z.object({
/** /**
* Create version metadata in persistent storage. * Create version metadata in persistent storage.
*/ */
export type IVersionCreateDTO = z.infer<typeof CreateVersionSchema>; export type IVersionCreateDTO = z.infer<typeof VersionCreateSchema>;
/** /**
* Represents data response when creating {@link IVersionInfo}. * Represents data response when creating {@link IVersionInfo}.
@ -109,6 +101,20 @@ export interface IVersionCreatedResponse {
schema: IRSFormDTO; schema: IRSFormDTO;
} }
/**
* Represents version data, intended to update version metadata in persistent storage.
*/
export const VersionUpdateSchema = z.object({
id: z.number(),
version: z.string().nonempty(errors.requiredField),
description: z.string()
});
/**
* Represents version data, intended to update version metadata in persistent storage.
*/
export type IVersionUpdateDTO = z.infer<typeof VersionUpdateSchema>;
export const libraryApi = { export const libraryApi = {
baseKey: 'library', baseKey: 'library',
libraryListKey: ['library', 'list'], libraryListKey: ['library', 'list'],
@ -234,9 +240,9 @@ export const libraryApi = {
successMessage: information.versionRestored successMessage: information.versionRestored
} }
}), }),
versionUpdate: ({ versionID, data }: { versionID: VersionID; data: IVersionData }) => versionUpdate: (data: IVersionUpdateDTO) =>
axiosPatch<IVersionData, IVersionInfo>({ axiosPatch<IVersionUpdateDTO, IVersionInfo>({
endpoint: `/api/versions/${versionID}`, endpoint: `/api/versions/${data.id}`,
request: { request: {
data: data, data: data,
successMessage: information.changesSaved successMessage: information.changesSaved

View File

@ -1,9 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api'; import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api';
import { IVersionData, VersionID } from '@/models/library';
import { libraryApi } from './api'; import { IVersionUpdateDTO, libraryApi } from './api';
export const useVersionUpdate = () => { export const useVersionUpdate = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -28,6 +27,6 @@ export const useVersionUpdate = () => {
} }
}); });
return { return {
versionUpdate: (data: { versionID: VersionID; data: IVersionData }) => mutation.mutate(data) versionUpdate: (data: IVersionUpdateDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess })
}; };
}; };

View File

@ -4,6 +4,9 @@ import { CProps } from '@/components/props';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
interface MiniButtonProps extends CProps.Button { interface MiniButtonProps extends CProps.Button {
/** Button type. */
type?: 'button' | 'submit';
/** Icon to display in the button. */ /** Icon to display in the button. */
icon: React.ReactNode; icon: React.ReactNode;
@ -25,12 +28,13 @@ export function MiniButton({
title, title,
titleHtml, titleHtml,
hideTitle, hideTitle,
type = 'button',
className, className,
...restProps ...restProps
}: MiniButtonProps) { }: MiniButtonProps) {
return ( return (
<button <button
type='button' type={type}
tabIndex={tabIndex ?? -1} tabIndex={tabIndex ?? -1}
className={clsx( className={clsx(
'rounded-lg', 'rounded-lg',

View File

@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { Controller, useForm, useWatch } from 'react-hook-form'; import { Controller, useForm, useWatch } from 'react-hook-form';
import { CreateVersionSchema, IVersionCreateDTO } from '@/backend/library/api'; import { VersionCreateSchema, IVersionCreateDTO } from '@/backend/library/api';
import { useVersionCreate } from '@/backend/library/useVersionCreate'; import { useVersionCreate } from '@/backend/library/useVersionCreate';
import { Checkbox, TextArea, TextInput } from '@/components/ui/Input'; import { Checkbox, TextArea, TextInput } from '@/components/ui/Input';
import { ModalForm } from '@/components/ui/Modal'; import { ModalForm } from '@/components/ui/Modal';
@ -33,7 +33,7 @@ function DlgCreateVersion() {
const { versionCreate } = useVersionCreate(); const { versionCreate } = useVersionCreate();
const { register, handleSubmit, control } = useForm<IVersionCreateDTO>({ const { register, handleSubmit, control } = useForm<IVersionCreateDTO>({
resolver: zodResolver(CreateVersionSchema), resolver: zodResolver(VersionCreateSchema),
defaultValues: { defaultValues: {
version: versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0', version: versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0',
description: '', description: '',

View File

@ -1,108 +1,119 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod';
import { useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { IVersionUpdateDTO, VersionUpdateSchema } from '@/backend/library/api';
import { useMutatingLibrary } from '@/backend/library/useMutatingLibrary'; import { useMutatingLibrary } from '@/backend/library/useMutatingLibrary';
import { useVersionDelete } from '@/backend/library/useVersionDelete'; import { useVersionDelete } from '@/backend/library/useVersionDelete';
import { useVersionUpdate } from '@/backend/library/useVersionUpdate'; import { useVersionUpdate } from '@/backend/library/useVersionUpdate';
import { useRSFormSuspense } from '@/backend/rsform/useRSForm';
import { IconReset, IconSave } from '@/components/Icons'; import { IconReset, IconSave } from '@/components/Icons';
import { MiniButton } from '@/components/ui/Control'; import { MiniButton } from '@/components/ui/Control';
import { TextArea, TextInput } from '@/components/ui/Input'; import { TextArea, TextInput } from '@/components/ui/Input';
import { ModalView } from '@/components/ui/Modal'; import { ModalView } from '@/components/ui/Modal';
import { ILibraryItemVersioned, IVersionInfo, VersionID } from '@/models/library'; import { LibraryItemID, VersionID } from '@/models/library';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import TableVersions from './TableVersions'; import TableVersions from './TableVersions';
export interface DlgEditVersionsProps { export interface DlgEditVersionsProps {
item: ILibraryItemVersioned; itemID: LibraryItemID;
afterDelete: (targetVersion: VersionID) => void; afterDelete: (targetVersion: VersionID) => void;
} }
function DlgEditVersions() { function DlgEditVersions() {
const { item, afterDelete } = useDialogsStore(state => state.props as DlgEditVersionsProps); const { itemID, afterDelete } = useDialogsStore(state => state.props as DlgEditVersionsProps);
const processing = useMutatingLibrary(); const hideDialog = useDialogsStore(state => state.hideDialog);
const { schema } = useRSFormSuspense({ itemID });
const isProcessing = useMutatingLibrary();
const { versionDelete } = useVersionDelete(); const { versionDelete } = useVersionDelete();
const { versionUpdate } = useVersionUpdate(); const { versionUpdate } = useVersionUpdate();
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined); const {
const [version, setVersion] = useState(''); register,
const [description, setDescription] = useState(''); handleSubmit,
control,
const isValid = selected && item.versions.every(ver => ver.id === selected.id || ver.version != version); reset,
const isModified = selected && (selected.version != version || selected.description != description); formState: { isDirty }
} = useForm<IVersionUpdateDTO>({
function handleDeleteVersion(targetVersion: VersionID) { resolver: zodResolver(VersionUpdateSchema),
versionDelete({ itemID: item.id, versionID: targetVersion }, () => afterDelete(targetVersion)); defaultValues: {
id: schema.versions[0].id,
version: schema.versions[0].version,
description: schema.versions[0].description
} }
});
const versionID = useWatch({ control, name: 'id' });
const versionName = useWatch({ control, name: 'version' });
function handleUpdate() { const isValid = useMemo(
if (!isModified || !selected || processing || !isValid) { () => schema.versions.every(ver => ver.id === versionID || ver.version != versionName),
[schema, versionID, versionName]
);
function handleSelectVersion(targetVersion: VersionID) {
const ver = schema.versions.find(ver => ver.id === targetVersion);
if (!ver) {
return; return;
} }
versionUpdate({ reset({ ...ver });
versionID: selected.id,
data: {
version: version,
description: description
} }
function handleDeleteVersion(targetVersion: VersionID) {
const nextVer = schema.versions.find(ver => ver.id !== targetVersion);
versionDelete({ itemID: itemID, versionID: targetVersion }, () => {
if (!nextVer) {
hideDialog();
} else if (targetVersion === versionID) {
reset({ id: nextVer.id, version: nextVer.version, description: nextVer.description });
}
afterDelete(targetVersion);
}); });
} }
function handleReset() { function onUpdate(data: IVersionUpdateDTO) {
if (!selected) { if (!isDirty || isProcessing || !isValid) {
return false; return;
} }
setVersion(selected?.version ?? ''); versionUpdate(data, () => reset({ ...data }));
setDescription(selected?.description ?? '');
} }
useEffect(() => {
setVersion(selected?.version ?? '');
setDescription(selected?.description ?? '');
}, [selected]);
return ( return (
<ModalView header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'> <ModalView header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
<TableVersions <TableVersions
processing={processing} processing={isProcessing}
items={item.versions} items={schema.versions}
onDelete={handleDeleteVersion} onDelete={handleDeleteVersion}
onSelect={versionID => setSelected(item.versions.find(ver => ver.id === versionID))} onSelect={handleSelectVersion}
selected={selected?.id} selected={versionID}
/> />
<div className='flex'> <form className='flex' onSubmit={event => void handleSubmit(onUpdate)(event)}>
<TextInput <TextInput id='dlg_version' {...register('version')} dense label='Версия' className='w-[16rem] mr-3' />
id='dlg_version'
dense
label='Версия'
className='w-[16rem] mr-3'
value={version}
onChange={event => setVersion(event.target.value)}
/>
<div className='cc-icons'> <div className='cc-icons'>
<MiniButton <MiniButton
type='submit'
title='Сохранить изменения' title='Сохранить изменения'
disabled={!isModified || !isValid || processing} disabled={!isDirty || !isValid || isProcessing}
icon={<IconSave size='1.25rem' className='icon-primary' />} icon={<IconSave size='1.25rem' className='icon-primary' />}
onClick={handleUpdate}
/> />
<MiniButton <MiniButton
title='Сбросить несохраненные изменения' title='Сбросить несохраненные изменения'
disabled={!isModified} disabled={!isDirty}
onClick={handleReset} onClick={() => reset()}
icon={<IconReset size='1.25rem' className='icon-primary' />} icon={<IconReset size='1.25rem' className='icon-primary' />}
/> />
</div> </div>
</div> </form>
<TextArea <TextArea
id='dlg_description' id='dlg_description' //
{...register('description')}
spellCheck spellCheck
label='Описание' label='Описание'
rows={3} rows={3}
value={description}
onChange={event => setDescription(event.target.value)}
/> />
</ModalView> </ModalView>
); );

View File

@ -21,6 +21,13 @@ const columnHelper = createColumnHelper<IVersionInfo>();
function TableVersions({ processing, items, onDelete, selected, onSelect }: TableVersionsProps) { function TableVersions({ processing, items, onDelete, selected, onSelect }: TableVersionsProps) {
const intl = useIntl(); const intl = useIntl();
function handleDeleteVersion(event: React.MouseEvent, targetVersion: VersionID) {
event.preventDefault();
event.stopPropagation();
onDelete(targetVersion);
}
const columns = [ const columns = [
columnHelper.accessor('version', { columnHelper.accessor('version', {
id: 'version', id: 'version',
@ -61,7 +68,7 @@ function TableVersions({ processing, items, onDelete, selected, onSelect }: Tabl
noPadding noPadding
disabled={processing} disabled={processing}
icon={<IconRemove size='1.25rem' className='icon-red' />} icon={<IconRemove size='1.25rem' className='icon-red' />}
onClick={() => onDelete(props.row.original.id)} onClick={event => handleDeleteVersion(event, props.row.original.id)}
/> />
</div> </div>
) )

View File

@ -54,14 +54,6 @@ export interface IVersionInfo {
time_create: string; time_create: string;
} }
/**
* Represents version data, intended to update version metadata in persistent storage.
*/
export interface IVersionData {
version: string;
description: string;
}
/** /**
* Represents library item common data typical for all item types. * Represents library item common data typical for all item types.
*/ */

View File

@ -48,7 +48,7 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
function handleEditVersions() { function handleEditVersions() {
showEditVersions({ showEditVersions({
item: controller.schema, itemID: controller.schema.id,
afterDelete: targetVersion => { afterDelete: targetVersion => {
if (targetVersion === controller.activeVersion) controller.navigateVersion(undefined); if (targetVersion === controller.activeVersion) controller.navigateVersion(undefined);
} }