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 { ossApi } from '@/backend/oss/api';
import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api';
import {
AccessPolicy,
ILibraryItem,
IVersionData,
IVersionInfo,
LibraryItemID,
LibraryItemType,
VersionID
} from '@/models/library';
import { AccessPolicy, ILibraryItem, IVersionInfo, LibraryItemID, LibraryItemType, VersionID } from '@/models/library';
import { validateLocation } from '@/models/libraryAPI';
import { ConstituentaID } from '@/models/rsform';
import { UserID } from '@/models/user';
@ -90,7 +82,7 @@ export type IUpdateLibraryItemDTO = z.infer<typeof UpdateLibraryItemSchema>;
/**
* Create version metadata in persistent storage.
*/
export const CreateVersionSchema = z.object({
export const VersionCreateSchema = z.object({
version: z.string(),
description: z.string(),
items: z.array(z.number()).optional()
@ -99,7 +91,7 @@ export const CreateVersionSchema = z.object({
/**
* 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}.
@ -109,6 +101,20 @@ export interface IVersionCreatedResponse {
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 = {
baseKey: 'library',
libraryListKey: ['library', 'list'],
@ -234,9 +240,9 @@ export const libraryApi = {
successMessage: information.versionRestored
}
}),
versionUpdate: ({ versionID, data }: { versionID: VersionID; data: IVersionData }) =>
axiosPatch<IVersionData, IVersionInfo>({
endpoint: `/api/versions/${versionID}`,
versionUpdate: (data: IVersionUpdateDTO) =>
axiosPatch<IVersionUpdateDTO, IVersionInfo>({
endpoint: `/api/versions/${data.id}`,
request: {
data: data,
successMessage: information.changesSaved

View File

@ -1,9 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
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 = () => {
const client = useQueryClient();
@ -28,6 +27,6 @@ export const useVersionUpdate = () => {
}
});
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';
interface MiniButtonProps extends CProps.Button {
/** Button type. */
type?: 'button' | 'submit';
/** Icon to display in the button. */
icon: React.ReactNode;
@ -25,12 +28,13 @@ export function MiniButton({
title,
titleHtml,
hideTitle,
type = 'button',
className,
...restProps
}: MiniButtonProps) {
return (
<button
type='button'
type={type}
tabIndex={tabIndex ?? -1}
className={clsx(
'rounded-lg',

View File

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

View File

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

View File

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

View File

@ -54,14 +54,6 @@ export interface IVersionInfo {
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.
*/

View File

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