F: Implementing PromptEdit pt2

This commit is contained in:
Ivan 2025-07-15 20:09:44 +03:00
parent 12c202adff
commit 56652095af
16 changed files with 220 additions and 117 deletions

View File

@ -17,7 +17,10 @@ class PromptTemplateSerializer(serializers.ModelSerializer):
def validate_label(self, value): def validate_label(self, value):
user = self.context['request'].user user = self.context['request'].user
if PromptTemplate.objects.filter(owner=user, label=value).exists(): queryset = PromptTemplate.objects.filter(owner=user, label=value)
if self.instance is not None:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise serializers.ValidationError(msg.promptLabelTaken(value)) raise serializers.ValidationError(msg.promptLabelTaken(value))
return value return value

View File

@ -1,11 +1,13 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { promptsApi } from './api'; import { promptsApi } from './api';
export function useCreatePromptTemplate() { export function useCreatePromptTemplate() {
const client = useQueryClient(); const client = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [promptsApi.baseKey, 'create'], mutationKey: [KEYS.global_mutation, promptsApi.baseKey, 'create'],
mutationFn: promptsApi.createPromptTemplate, mutationFn: promptsApi.createPromptTemplate,
onSuccess: () => { onSuccess: () => {
void client.invalidateQueries({ queryKey: [promptsApi.baseKey] }); void client.invalidateQueries({ queryKey: [promptsApi.baseKey] });

View File

@ -1,13 +1,15 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { promptsApi } from './api'; import { promptsApi } from './api';
export function useDeletePromptTemplate() { export function useDeletePromptTemplate() {
const client = useQueryClient(); const client = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [promptsApi.baseKey, 'delete'], mutationKey: [KEYS.global_mutation, promptsApi.baseKey, 'delete'],
mutationFn: promptsApi.deletePromptTemplate, mutationFn: promptsApi.deletePromptTemplate,
onSuccess: (_data, id) => { onSuccess: (_, id) => {
void client.invalidateQueries({ queryKey: [promptsApi.baseKey] }); void client.invalidateQueries({ queryKey: [promptsApi.baseKey] });
void client.invalidateQueries({ queryKey: [promptsApi.baseKey, id] }); void client.invalidateQueries({ queryKey: [promptsApi.baseKey, id] });
} }

View File

@ -0,0 +1,10 @@
import { useIsMutating } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { promptsApi } from './api';
export const useMutatingPrompts = () => {
const countMutations = useIsMutating({ mutationKey: [KEYS.global_mutation, promptsApi.baseKey] });
return countMutations !== 0;
};

View File

@ -1,16 +1,21 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { promptsApi } from './api'; import { promptsApi } from './api';
import { type IUpdatePromptTemplateDTO } from './types'; import { type IPromptTemplateDTO, type IUpdatePromptTemplateDTO } from './types';
export function useUpdatePromptTemplate() { export function useUpdatePromptTemplate() {
const client = useQueryClient(); const client = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [promptsApi.baseKey, 'update'], mutationKey: [KEYS.global_mutation, promptsApi.baseKey, 'update'],
mutationFn: ({ id, data }: { id: number; data: IUpdatePromptTemplateDTO }) => mutationFn: ({ id, data }: { id: number; data: IUpdatePromptTemplateDTO }) =>
promptsApi.updatePromptTemplate(id, data), promptsApi.updatePromptTemplate(id, data),
onSuccess: (_, variables) => { onSuccess: (data: IPromptTemplateDTO) => {
void client.invalidateQueries({ queryKey: [promptsApi.baseKey, variables.id] }); client.setQueryData(promptsApi.getPromptTemplateQueryOptions(data.id).queryKey, data);
client.setQueryData(promptsApi.getAvailableTemplatesQueryOptions().queryKey, prev =>
prev?.map(item => (item.id === data.id ? data : item))
);
} }
}); });
return { return {

View File

@ -3,7 +3,7 @@ import { type DomIconProps, IconPrivate, IconPublic } from '@/components/icons';
/** Icon for shared template flag. */ /** Icon for shared template flag. */
export function IconSharedTemplate({ value, size = '1.25rem', className }: DomIconProps<boolean>) { export function IconSharedTemplate({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
if (value) { if (value) {
return <IconPublic size={size} className={className ?? 'text-primary'} />; return <IconPublic size={size} className={className ?? 'text-constructive'} />;
} else { } else {
return <IconPrivate size={size} className={className ?? 'text-primary'} />; return <IconPrivate size={size} className={className ?? 'text-primary'} />;
} }

View File

@ -44,6 +44,7 @@ export function DlgCreatePromptTemplate() {
submitText='Создать' submitText='Создать'
canSubmit={isValid} canSubmit={isValid}
onSubmit={event => void handleSubmit(onSubmit)(event)} onSubmit={event => void handleSubmit(onSubmit)(event)}
submitInvalidTooltip='Введите уникальное название шаблона'
className='cc-column w-140 max-h-120 py-2 px-6' className='cc-column w-140 max-h-120 py-2 px-6'
> >
<TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' /> <TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' />

View File

@ -1,8 +1,13 @@
import { ErrorBoundary } from 'react-error-boundary';
import { isAxiosError } from 'axios';
import { z } from 'zod'; import { z } from 'zod';
import { useBlockNavigation } from '@/app'; import { useBlockNavigation } from '@/app';
import { routes } from '@/app/urls';
import { RequireAuth } from '@/features/auth/components/require-auth'; import { RequireAuth } from '@/features/auth/components/require-auth';
import { TextURL } from '@/components/control';
import { type ErrorData } from '@/components/info-error';
import { useQueryStrings } from '@/hooks/use-query-strings'; import { useQueryStrings } from '@/hooks/use-query-strings';
import { useModificationStore } from '@/stores/modification'; import { useModificationStore } from '@/stores/modification';
@ -25,10 +30,29 @@ export function PromptTemplatesPage() {
useBlockNavigation(isModified); useBlockNavigation(isModified);
return ( return (
<RequireAuth> <ErrorBoundary
<TemplatesTabs activeID={urlData.active} tab={urlData.tab} /> FallbackComponent={({ error }) => <ProcessError error={error as ErrorData} itemID={urlData.active} />}
</RequireAuth> >
<RequireAuth>
<TemplatesTabs activeID={urlData.active} tab={urlData.tab} />
</RequireAuth>
</ErrorBoundary>
); );
} }
export default PromptTemplatesPage; // ====== Internals =========
function ProcessError({ error, itemID }: { error: ErrorData; itemID?: number | null }): React.ReactElement | null {
if (isAxiosError(error) && error.response) {
if (error.response.status === 404) {
return (
<div className='flex flex-col items-center p-2 mx-auto'>
<p>{`Шаблон запроса с указанным идентификатором ${itemID} отсутствует`}</p>
<div className='flex justify-center'>
<TextURL text='Список шаблонов' href={`/${routes.prompt_templates}`} />
</div>
</div>
);
}
}
throw error as Error;
}

View File

@ -1,13 +1,18 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { useEffect } from 'react'; import { useRef } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';
import { useUpdatePromptTemplate } from '@/features/ai/backend/use-update-prompt-template';
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth';
import { Checkbox, TextArea, TextInput } from '@/components/input'; import { Checkbox, TextArea, TextInput } from '@/components/input';
import { cn } from '@/components/utils'; import { cn } from '@/components/utils';
import { useModificationStore } from '@/stores/modification';
import { globalIDs } from '@/utils/constants';
import { import {
type IPromptTemplate, type IPromptTemplate,
@ -17,31 +22,24 @@ import {
interface FormPromptTemplateProps { interface FormPromptTemplateProps {
promptTemplate: IPromptTemplate; promptTemplate: IPromptTemplate;
disabled: boolean; isMutable: boolean;
onSubmit: (data: IUpdatePromptTemplateDTO) => void;
onReset: () => void;
className?: string; className?: string;
toggleReset: boolean;
} }
/** Form for editing a prompt template. */ /** Form for editing a prompt template. */
export function FormPromptTemplate({ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggleReset }: FormPromptTemplateProps) {
promptTemplate,
disabled,
className,
onSubmit,
onReset: _onReset
}: FormPromptTemplateProps) {
const { user } = useAuthSuspense(); const { user } = useAuthSuspense();
const isProcessing = useMutatingPrompts();
const setIsModified = useModificationStore(state => state.setIsModified);
const { updatePromptTemplate } = useUpdatePromptTemplate();
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
register, register,
formState: { formState: { isDirty, errors }
/* isDirty, */
/* errors */
}
} = useForm<IUpdatePromptTemplateDTO>({ } = useForm<IUpdatePromptTemplateDTO>({
resolver: zodResolver(schemaUpdatePromptTemplate), resolver: zodResolver(schemaUpdatePromptTemplate),
defaultValues: { defaultValues: {
@ -53,7 +51,11 @@ export function FormPromptTemplate({
} }
}); });
useEffect(() => { const prevReset = useRef(toggleReset);
const prevTemplate = useRef(promptTemplate);
if (prevTemplate.current !== promptTemplate || prevReset.current !== toggleReset) {
prevTemplate.current = promptTemplate;
prevReset.current = toggleReset;
reset({ reset({
owner: promptTemplate.owner, owner: promptTemplate.owner,
label: promptTemplate.label, label: promptTemplate.label,
@ -61,30 +63,64 @@ export function FormPromptTemplate({
text: promptTemplate.text, text: promptTemplate.text,
is_shared: promptTemplate.is_shared is_shared: promptTemplate.is_shared
}); });
}, [promptTemplate, reset]); }
const prevDirty = useRef(isDirty);
if (prevDirty.current !== isDirty) {
prevDirty.current = isDirty;
setIsModified(isDirty);
}
function onSubmit(data: IUpdatePromptTemplateDTO) {
return updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {
setIsModified(false);
reset({ ...data });
});
}
return ( return (
<form className={cn('flex flex-col gap-3 px-6', className)} onSubmit={event => void handleSubmit(onSubmit)(event)}> <form
<TextInput id='prompt_label' label='Название' {...register('label')} disabled={disabled} /> id={globalIDs.prompt_editor}
<TextArea id='prompt_description' label='Описание' {...register('description')} disabled={disabled} /> className={cn('flex flex-col gap-3 px-6', className)}
<TextArea id='prompt_text' label='Содержание' {...register('text')} disabled={disabled} /> onSubmit={event => void handleSubmit(onSubmit)(event)}
{user.is_staff ? ( >
<Controller <TextInput
name='is_shared' id='prompt_label'
control={control} label='Название' //
render={({ field }) => ( {...register('label')}
<Checkbox error={errors.label}
id='prompt_is_shared' disabled={isProcessing || !isMutable}
label='Общий шаблон' />
value={field.value} <TextArea
onChange={field.onChange} id='prompt_description'
onBlur={field.onBlur} label='Описание' //
ref={field.ref} {...register('description')}
disabled={disabled} error={errors.description}
/> disabled={isProcessing || !isMutable}
)} />
/> <TextArea
) : null} id='prompt_text'
label='Содержание' //
{...register('text')}
error={errors.text}
disabled={isProcessing || !isMutable}
/>
<Controller
name='is_shared'
control={control}
render={({ field }) => (
<Checkbox
id='prompt_is_shared'
label='Общий шаблон'
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
ref={field.ref}
disabled={isProcessing || !isMutable || !user.is_staff}
/>
)}
/>
</form> </form>
); );
} }

View File

@ -1,11 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import clsx from 'clsx';
import { promptText } from '@/utils/labels'; import { useAuthSuspense } from '@/features/auth';
import { useModificationStore } from '@/stores/modification';
import { globalIDs } from '@/utils/constants';
import { type IUpdatePromptTemplateDTO } from '../../../backend/types';
import { useDeletePromptTemplate } from '../../../backend/use-delete-prompt-template';
import { usePromptTemplateSuspense } from '../../../backend/use-prompt-template'; import { usePromptTemplateSuspense } from '../../../backend/use-prompt-template';
import { useUpdatePromptTemplate } from '../../../backend/use-update-prompt-template';
import { FormPromptTemplate } from './form-prompt-template'; import { FormPromptTemplate } from './form-prompt-template';
import { ToolbarTemplate } from './toolbar-template'; import { ToolbarTemplate } from './toolbar-template';
@ -16,53 +17,52 @@ interface TabEditTemplateProps {
export function TabEditTemplate({ activeID }: TabEditTemplateProps) { export function TabEditTemplate({ activeID }: TabEditTemplateProps) {
const { promptTemplate } = usePromptTemplateSuspense(activeID); const { promptTemplate } = usePromptTemplateSuspense(activeID);
const { updatePromptTemplate, isPending: isSaving } = useUpdatePromptTemplate(); const isModified = useModificationStore(state => state.isModified);
const { deletePromptTemplate, isPending: isDeleting } = useDeletePromptTemplate(); const setIsModified = useModificationStore(state => state.setIsModified);
const [isModified, setIsModified] = useState(false); const { user } = useAuthSuspense();
const [formKey, setFormKey] = useState(0); const isMutable = user.is_staff || promptTemplate.owner === user.id;
const [toggleReset, setToggleReset] = useState(false);
function handleSave(data: IUpdatePromptTemplateDTO) {
void updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {
setIsModified(false);
setFormKey(k => k + 1); // force form reset
});
}
function handleReset() { function handleReset() {
setFormKey(k => k + 1); setToggleReset(t => !t);
setIsModified(false); setIsModified(false);
} }
function handleDelete() {
if (window.confirm(promptText.deleteTemplate)) {
void deletePromptTemplate(promptTemplate.id);
}
}
function triggerFormSubmit() { function triggerFormSubmit() {
const form = document.getElementById('prompt-template-edit-form') as HTMLFormElement | null; const form = document.getElementById(globalIDs.prompt_editor) as HTMLFormElement | null;
if (form) { if (form) {
form.requestSubmit(); form.requestSubmit();
} }
} }
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
if (isModified) {
triggerFormSubmit();
}
event.preventDefault();
return;
}
}
return ( return (
<div className='pt-8 rounded bg-background relative'> <div className='pt-8 rounded bg-background relative' tabIndex={-1} onKeyDown={handleInput}>
<ToolbarTemplate {isMutable ? (
className='cc-tab-tools cc-animate-position right-1/2 translate-x-0 xs:right-4 xs:-translate-x-1/2 md:right-1/2 md:translate-x-0' <ToolbarTemplate
disabled={isSaving || isDeleting} activeID={activeID}
isModified={isModified} className={clsx(
onSave={triggerFormSubmit} 'cc-tab-tools cc-animate-position',
onReset={handleReset} 'right-1/2 translate-x-0 xs:right-4 xs:-translate-x-1/2 md:right-1/2 md:translate-x-0'
onDelete={handleDelete} )}
/> onSave={triggerFormSubmit}
onReset={handleReset}
/>
) : null}
<FormPromptTemplate <FormPromptTemplate
className='mt-12 xs:mt-4 w-100 md:w-180 min-w-70' className='mt-12 xs:mt-4 w-100 md:w-180 min-w-70'
key={formKey} isMutable={isMutable}
promptTemplate={promptTemplate} promptTemplate={promptTemplate}
disabled={isSaving || isDeleting} toggleReset={toggleReset}
onSubmit={handleSave}
onReset={handleReset}
/> />
</div> </div>
); );

View File

@ -1,40 +1,60 @@
import { urls, useConceptNavigation } from '@/app';
import { useDeletePromptTemplate } from '@/features/ai/backend/use-delete-prompt-template';
import { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';
import { MiniButton } from '@/components/control'; import { MiniButton } from '@/components/control';
import { IconDestroy, IconReset, IconSave } from '@/components/icons'; import { IconDestroy, IconReset, IconSave } from '@/components/icons';
import { cn } from '@/components/utils'; import { cn } from '@/components/utils';
import { useModificationStore } from '@/stores/modification';
import { promptText } from '@/utils/labels';
import { isMac, prepareTooltip } from '@/utils/utils';
import { PromptTabID } from '../templates-tabs';
interface ToolbarTemplateProps { interface ToolbarTemplateProps {
disabled: boolean; activeID: number;
isModified: boolean;
onSave: () => void; onSave: () => void;
onReset: () => void; onReset: () => void;
onDelete: () => void;
className?: string; className?: string;
} }
/** Toolbar for prompt template editing. */ /** Toolbar for prompt template editing. */
export function ToolbarTemplate({ disabled, isModified, onSave, onReset, onDelete, className }: ToolbarTemplateProps) { export function ToolbarTemplate({ activeID, onSave, onReset, className }: ToolbarTemplateProps) {
const router = useConceptNavigation();
const { deletePromptTemplate } = useDeletePromptTemplate();
const isProcessing = useMutatingPrompts();
const isModified = useModificationStore(state => state.isModified);
function handleDelete() {
if (window.confirm(promptText.deleteTemplate)) {
void deletePromptTemplate(activeID).then(() =>
router.pushAsync({ path: urls.prompt_template(null, PromptTabID.LIST) })
);
}
}
return ( return (
<div className={cn('cc-icons items-start outline-hidden', className)}> <div className={cn('cc-icons items-start outline-hidden', className)}>
<MiniButton <MiniButton
title='Сохранить изменения' titleHtml={prepareTooltip('Сохранить изменения', isMac() ? 'Cmd + S' : 'Ctrl + S')}
aria-label='Сохранить изменения' aria-label='Сохранить изменения'
icon={<IconSave size='1.25rem' className='icon-primary' />} icon={<IconSave size='1.25rem' className='icon-primary' />}
onClick={onSave} onClick={onSave}
disabled={disabled || !isModified} disabled={isProcessing || !isModified}
/> />
<MiniButton <MiniButton
title='Сбросить изменения' title='Сбросить изменения'
aria-label='Сбросить изменения' aria-label='Сбросить изменения'
icon={<IconReset size='1.25rem' className='icon-primary' />} icon={<IconReset size='1.25rem' className='icon-primary' />}
onClick={onReset} onClick={onReset}
disabled={disabled || !isModified} disabled={isProcessing || !isModified}
/> />
<MiniButton <MiniButton
title='Удалить шаблон' title='Удалить шаблон'
aria-label='Удалить шаблон' aria-label='Удалить шаблон'
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
onClick={onDelete} onClick={handleDelete}
disabled={disabled} disabled={isProcessing}
/> />
</div> </div>
); );

View File

@ -26,10 +26,15 @@ export function TabListTemplates({ activeID }: TabListTemplatesProps) {
const getUserLabel = useLabelUser(); const getUserLabel = useLabelUser();
function handleRowDoubleClicked(row: RO<IPromptTemplate>, event: React.MouseEvent<Element>) { function handleRowDoubleClicked(row: RO<IPromptTemplate>, event: React.MouseEvent<Element>) {
event.preventDefault();
event.stopPropagation();
router.push({ path: urls.prompt_template(row.id, PromptTabID.EDIT), newTab: event.ctrlKey || event.metaKey }); router.push({ path: urls.prompt_template(row.id, PromptTabID.EDIT), newTab: event.ctrlKey || event.metaKey });
} }
function handleRowClicked(row: RO<IPromptTemplate>, event: React.MouseEvent<Element>) { function handleRowClicked(row: RO<IPromptTemplate>, event: React.MouseEvent<Element>) {
if (row.id === activeID) {
return;
}
router.push({ path: urls.prompt_template(row.id, PromptTabID.LIST), newTab: event.ctrlKey || event.metaKey }); router.push({ path: urls.prompt_template(row.id, PromptTabID.LIST), newTab: event.ctrlKey || event.metaKey });
} }

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler 'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { useEffect, useRef } from 'react'; import { useRef } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -47,12 +47,10 @@ export function FormOSS() {
const readOnly = useWatch({ control, name: 'read_only' }); const readOnly = useWatch({ control, name: 'read_only' });
const prevDirty = useRef(isDirty); const prevDirty = useRef(isDirty);
useEffect(() => { if (prevDirty.current !== isDirty) {
if (prevDirty.current !== isDirty) { prevDirty.current = isDirty;
prevDirty.current = isDirty; setIsModified(isDirty);
setIsModified(isDirty); }
}
}, [isDirty, setIsModified]);
function onSubmit(data: IUpdateLibraryItemDTO) { function onSubmit(data: IUpdateLibraryItemDTO) {
return updateOss(data).then(() => reset({ ...data })); return updateOss(data).then(() => reset({ ...data }));

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler 'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -121,12 +121,10 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
} }
const prevDirty = useRef(isDirty); const prevDirty = useRef(isDirty);
useEffect(() => { if (prevDirty.current !== isDirty) {
if (prevDirty.current !== isDirty) { prevDirty.current = isDirty;
prevDirty.current = isDirty; setIsModified(isDirty);
setIsModified(isDirty); }
}
}, [isDirty, setIsModified]);
function onSubmit(data: IUpdateConstituentaDTO) { function onSubmit(data: IUpdateConstituentaDTO) {
void cstUpdate({ itemID: schema.id, data }).then(() => { void cstUpdate({ itemID: schema.id, data }).then(() => {

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler 'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { useEffect, useRef } from 'react'; import { useRef } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -71,12 +71,10 @@ export function FormRSForm() {
} }
const prevDirty = useRef(isDirty); const prevDirty = useRef(isDirty);
useEffect(() => { if (prevDirty.current !== isDirty) {
if (prevDirty.current !== isDirty) { prevDirty.current = isDirty;
prevDirty.current = isDirty; setIsModified(isDirty);
setIsModified(isDirty); }
}
}, [isDirty, setIsModified]);
function handleSelectVersion(version: CurrentVersion) { function handleSelectVersion(version: CurrentVersion) {
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) }); router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });

View File

@ -78,6 +78,7 @@ export const globalIDs = {
email_tooltip: 'email_tooltip', email_tooltip: 'email_tooltip',
library_item_editor: 'library_item_editor', library_item_editor: 'library_item_editor',
constituenta_editor: 'constituenta_editor', constituenta_editor: 'constituenta_editor',
prompt_editor: 'prompt_editor',
graph_schemas: 'graph_schemas_tooltip', graph_schemas: 'graph_schemas_tooltip',
user_dropdown: 'user_dropdown', user_dropdown: 'user_dropdown',
ai_dropdown: 'ai_dropdown' ai_dropdown: 'ai_dropdown'