F: Rework version editing
This commit is contained in:
parent
0320dd68ab
commit
6187146e86
|
@ -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
|
||||||
|
|
|
@ -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 })
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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: '',
|
||||||
|
|
|
@ -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,
|
||||||
|
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' });
|
||||||
|
|
||||||
const isValid = selected && item.versions.every(ver => ver.id === selected.id || ver.version != version);
|
const isValid = useMemo(
|
||||||
const isModified = selected && (selected.version != version || selected.description != description);
|
() => schema.versions.every(ver => ver.id === versionID || ver.version != versionName),
|
||||||
|
[schema, versionID, versionName]
|
||||||
|
);
|
||||||
|
|
||||||
function handleDeleteVersion(targetVersion: VersionID) {
|
function handleSelectVersion(targetVersion: VersionID) {
|
||||||
versionDelete({ itemID: item.id, versionID: targetVersion }, () => afterDelete(targetVersion));
|
const ver = schema.versions.find(ver => ver.id === targetVersion);
|
||||||
}
|
if (!ver) {
|
||||||
|
|
||||||
function handleUpdate() {
|
|
||||||
if (!isModified || !selected || processing || !isValid) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
versionUpdate({
|
reset({ ...ver });
|
||||||
versionID: selected.id,
|
}
|
||||||
data: {
|
|
||||||
version: version,
|
function handleDeleteVersion(targetVersion: VersionID) {
|
||||||
description: description
|
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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user