F: Implementing PromptEdit pt2
This commit is contained in:
parent
12c202adff
commit
56652095af
|
@ -17,7 +17,10 @@ class PromptTemplateSerializer(serializers.ModelSerializer):
|
|||
|
||||
def validate_label(self, value):
|
||||
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))
|
||||
return value
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { promptsApi } from './api';
|
||||
|
||||
export function useCreatePromptTemplate() {
|
||||
const client = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationKey: [promptsApi.baseKey, 'create'],
|
||||
mutationKey: [KEYS.global_mutation, promptsApi.baseKey, 'create'],
|
||||
mutationFn: promptsApi.createPromptTemplate,
|
||||
onSuccess: () => {
|
||||
void client.invalidateQueries({ queryKey: [promptsApi.baseKey] });
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { promptsApi } from './api';
|
||||
|
||||
export function useDeletePromptTemplate() {
|
||||
const client = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationKey: [promptsApi.baseKey, 'delete'],
|
||||
mutationKey: [KEYS.global_mutation, promptsApi.baseKey, 'delete'],
|
||||
mutationFn: promptsApi.deletePromptTemplate,
|
||||
onSuccess: (_data, id) => {
|
||||
onSuccess: (_, id) => {
|
||||
void client.invalidateQueries({ queryKey: [promptsApi.baseKey] });
|
||||
void client.invalidateQueries({ queryKey: [promptsApi.baseKey, id] });
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -1,16 +1,21 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { promptsApi } from './api';
|
||||
import { type IUpdatePromptTemplateDTO } from './types';
|
||||
import { type IPromptTemplateDTO, type IUpdatePromptTemplateDTO } from './types';
|
||||
|
||||
export function useUpdatePromptTemplate() {
|
||||
const client = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationKey: [promptsApi.baseKey, 'update'],
|
||||
mutationKey: [KEYS.global_mutation, promptsApi.baseKey, 'update'],
|
||||
mutationFn: ({ id, data }: { id: number; data: IUpdatePromptTemplateDTO }) =>
|
||||
promptsApi.updatePromptTemplate(id, data),
|
||||
onSuccess: (_, variables) => {
|
||||
void client.invalidateQueries({ queryKey: [promptsApi.baseKey, variables.id] });
|
||||
onSuccess: (data: IPromptTemplateDTO) => {
|
||||
client.setQueryData(promptsApi.getPromptTemplateQueryOptions(data.id).queryKey, data);
|
||||
client.setQueryData(promptsApi.getAvailableTemplatesQueryOptions().queryKey, prev =>
|
||||
prev?.map(item => (item.id === data.id ? data : item))
|
||||
);
|
||||
}
|
||||
});
|
||||
return {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { type DomIconProps, IconPrivate, IconPublic } from '@/components/icons';
|
|||
/** Icon for shared template flag. */
|
||||
export function IconSharedTemplate({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||
if (value) {
|
||||
return <IconPublic size={size} className={className ?? 'text-primary'} />;
|
||||
return <IconPublic size={size} className={className ?? 'text-constructive'} />;
|
||||
} else {
|
||||
return <IconPrivate size={size} className={className ?? 'text-primary'} />;
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ export function DlgCreatePromptTemplate() {
|
|||
submitText='Создать'
|
||||
canSubmit={isValid}
|
||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||
submitInvalidTooltip='Введите уникальное название шаблона'
|
||||
className='cc-column w-140 max-h-120 py-2 px-6'
|
||||
>
|
||||
<TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' />
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useBlockNavigation } from '@/app';
|
||||
import { routes } from '@/app/urls';
|
||||
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 { useModificationStore } from '@/stores/modification';
|
||||
|
||||
|
@ -25,10 +30,29 @@ export function PromptTemplatesPage() {
|
|||
useBlockNavigation(isModified);
|
||||
|
||||
return (
|
||||
<RequireAuth>
|
||||
<TemplatesTabs activeID={urlData.active} tab={urlData.tab} />
|
||||
</RequireAuth>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={({ error }) => <ProcessError error={error as ErrorData} itemID={urlData.active} />}
|
||||
>
|
||||
<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;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
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 { Checkbox, TextArea, TextInput } from '@/components/input';
|
||||
import { cn } from '@/components/utils';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import {
|
||||
type IPromptTemplate,
|
||||
|
@ -17,31 +22,24 @@ import {
|
|||
|
||||
interface FormPromptTemplateProps {
|
||||
promptTemplate: IPromptTemplate;
|
||||
disabled: boolean;
|
||||
onSubmit: (data: IUpdatePromptTemplateDTO) => void;
|
||||
onReset: () => void;
|
||||
isMutable: boolean;
|
||||
className?: string;
|
||||
toggleReset: boolean;
|
||||
}
|
||||
|
||||
/** Form for editing a prompt template. */
|
||||
export function FormPromptTemplate({
|
||||
promptTemplate,
|
||||
disabled,
|
||||
className,
|
||||
onSubmit,
|
||||
onReset: _onReset
|
||||
}: FormPromptTemplateProps) {
|
||||
export function FormPromptTemplate({ promptTemplate, className, isMutable, toggleReset }: FormPromptTemplateProps) {
|
||||
const { user } = useAuthSuspense();
|
||||
const isProcessing = useMutatingPrompts();
|
||||
const setIsModified = useModificationStore(state => state.setIsModified);
|
||||
const { updatePromptTemplate } = useUpdatePromptTemplate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
register,
|
||||
formState: {
|
||||
/* isDirty, */
|
||||
/* errors */
|
||||
}
|
||||
formState: { isDirty, errors }
|
||||
} = useForm<IUpdatePromptTemplateDTO>({
|
||||
resolver: zodResolver(schemaUpdatePromptTemplate),
|
||||
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({
|
||||
owner: promptTemplate.owner,
|
||||
label: promptTemplate.label,
|
||||
|
@ -61,30 +63,64 @@ export function FormPromptTemplate({
|
|||
text: promptTemplate.text,
|
||||
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 (
|
||||
<form className={cn('flex flex-col gap-3 px-6', className)} onSubmit={event => void handleSubmit(onSubmit)(event)}>
|
||||
<TextInput id='prompt_label' label='Название' {...register('label')} disabled={disabled} />
|
||||
<TextArea id='prompt_description' label='Описание' {...register('description')} disabled={disabled} />
|
||||
<TextArea id='prompt_text' label='Содержание' {...register('text')} disabled={disabled} />
|
||||
{user.is_staff ? (
|
||||
<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={disabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<form
|
||||
id={globalIDs.prompt_editor}
|
||||
className={cn('flex flex-col gap-3 px-6', className)}
|
||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||
>
|
||||
<TextInput
|
||||
id='prompt_label'
|
||||
label='Название' //
|
||||
{...register('label')}
|
||||
error={errors.label}
|
||||
disabled={isProcessing || !isMutable}
|
||||
/>
|
||||
<TextArea
|
||||
id='prompt_description'
|
||||
label='Описание' //
|
||||
{...register('description')}
|
||||
error={errors.description}
|
||||
disabled={isProcessing || !isMutable}
|
||||
/>
|
||||
<TextArea
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
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 { useUpdatePromptTemplate } from '../../../backend/use-update-prompt-template';
|
||||
|
||||
import { FormPromptTemplate } from './form-prompt-template';
|
||||
import { ToolbarTemplate } from './toolbar-template';
|
||||
|
@ -16,53 +17,52 @@ interface TabEditTemplateProps {
|
|||
|
||||
export function TabEditTemplate({ activeID }: TabEditTemplateProps) {
|
||||
const { promptTemplate } = usePromptTemplateSuspense(activeID);
|
||||
const { updatePromptTemplate, isPending: isSaving } = useUpdatePromptTemplate();
|
||||
const { deletePromptTemplate, isPending: isDeleting } = useDeletePromptTemplate();
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
|
||||
function handleSave(data: IUpdatePromptTemplateDTO) {
|
||||
void updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {
|
||||
setIsModified(false);
|
||||
setFormKey(k => k + 1); // force form reset
|
||||
});
|
||||
}
|
||||
const isModified = useModificationStore(state => state.isModified);
|
||||
const setIsModified = useModificationStore(state => state.setIsModified);
|
||||
const { user } = useAuthSuspense();
|
||||
const isMutable = user.is_staff || promptTemplate.owner === user.id;
|
||||
const [toggleReset, setToggleReset] = useState(false);
|
||||
|
||||
function handleReset() {
|
||||
setFormKey(k => k + 1);
|
||||
setToggleReset(t => !t);
|
||||
setIsModified(false);
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (window.confirm(promptText.deleteTemplate)) {
|
||||
void deletePromptTemplate(promptTemplate.id);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
form.requestSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||
if (isModified) {
|
||||
triggerFormSubmit();
|
||||
}
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='pt-8 rounded bg-background relative'>
|
||||
<ToolbarTemplate
|
||||
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'
|
||||
disabled={isSaving || isDeleting}
|
||||
isModified={isModified}
|
||||
onSave={triggerFormSubmit}
|
||||
onReset={handleReset}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
<div className='pt-8 rounded bg-background relative' tabIndex={-1} onKeyDown={handleInput}>
|
||||
{isMutable ? (
|
||||
<ToolbarTemplate
|
||||
activeID={activeID}
|
||||
className={clsx(
|
||||
'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'
|
||||
)}
|
||||
onSave={triggerFormSubmit}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
) : null}
|
||||
<FormPromptTemplate
|
||||
className='mt-12 xs:mt-4 w-100 md:w-180 min-w-70'
|
||||
key={formKey}
|
||||
isMutable={isMutable}
|
||||
promptTemplate={promptTemplate}
|
||||
disabled={isSaving || isDeleting}
|
||||
onSubmit={handleSave}
|
||||
onReset={handleReset}
|
||||
toggleReset={toggleReset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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 { IconDestroy, IconReset, IconSave } from '@/components/icons';
|
||||
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 {
|
||||
disabled: boolean;
|
||||
isModified: boolean;
|
||||
activeID: number;
|
||||
onSave: () => void;
|
||||
onReset: () => void;
|
||||
onDelete: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 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 (
|
||||
<div className={cn('cc-icons items-start outline-hidden', className)}>
|
||||
<MiniButton
|
||||
title='Сохранить изменения'
|
||||
titleHtml={prepareTooltip('Сохранить изменения', isMac() ? 'Cmd + S' : 'Ctrl + S')}
|
||||
aria-label='Сохранить изменения'
|
||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||
onClick={onSave}
|
||||
disabled={disabled || !isModified}
|
||||
disabled={isProcessing || !isModified}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Сбросить изменения'
|
||||
aria-label='Сбросить изменения'
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={onReset}
|
||||
disabled={disabled || !isModified}
|
||||
disabled={isProcessing || !isModified}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Удалить шаблон'
|
||||
aria-label='Удалить шаблон'
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
onClick={onDelete}
|
||||
disabled={disabled}
|
||||
onClick={handleDelete}
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -26,10 +26,15 @@ export function TabListTemplates({ activeID }: TabListTemplatesProps) {
|
|||
const getUserLabel = useLabelUser();
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
|
@ -47,12 +47,10 @@ export function FormOSS() {
|
|||
const readOnly = useWatch({ control, name: 'read_only' });
|
||||
|
||||
const prevDirty = useRef(isDirty);
|
||||
useEffect(() => {
|
||||
if (prevDirty.current !== isDirty) {
|
||||
prevDirty.current = isDirty;
|
||||
setIsModified(isDirty);
|
||||
}
|
||||
}, [isDirty, setIsModified]);
|
||||
if (prevDirty.current !== isDirty) {
|
||||
prevDirty.current = isDirty;
|
||||
setIsModified(isDirty);
|
||||
}
|
||||
|
||||
function onSubmit(data: IUpdateLibraryItemDTO) {
|
||||
return updateOss(data).then(() => reset({ ...data }));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-toastify';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
@ -121,12 +121,10 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
|
|||
}
|
||||
|
||||
const prevDirty = useRef(isDirty);
|
||||
useEffect(() => {
|
||||
if (prevDirty.current !== isDirty) {
|
||||
prevDirty.current = isDirty;
|
||||
setIsModified(isDirty);
|
||||
}
|
||||
}, [isDirty, setIsModified]);
|
||||
if (prevDirty.current !== isDirty) {
|
||||
prevDirty.current = isDirty;
|
||||
setIsModified(isDirty);
|
||||
}
|
||||
|
||||
function onSubmit(data: IUpdateConstituentaDTO) {
|
||||
void cstUpdate({ itemID: schema.id, data }).then(() => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
|
@ -71,12 +71,10 @@ export function FormRSForm() {
|
|||
}
|
||||
|
||||
const prevDirty = useRef(isDirty);
|
||||
useEffect(() => {
|
||||
if (prevDirty.current !== isDirty) {
|
||||
prevDirty.current = isDirty;
|
||||
setIsModified(isDirty);
|
||||
}
|
||||
}, [isDirty, setIsModified]);
|
||||
if (prevDirty.current !== isDirty) {
|
||||
prevDirty.current = isDirty;
|
||||
setIsModified(isDirty);
|
||||
}
|
||||
|
||||
function handleSelectVersion(version: CurrentVersion) {
|
||||
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });
|
||||
|
|
|
@ -78,6 +78,7 @@ export const globalIDs = {
|
|||
email_tooltip: 'email_tooltip',
|
||||
library_item_editor: 'library_item_editor',
|
||||
constituenta_editor: 'constituenta_editor',
|
||||
prompt_editor: 'prompt_editor',
|
||||
graph_schemas: 'graph_schemas_tooltip',
|
||||
user_dropdown: 'user_dropdown',
|
||||
ai_dropdown: 'ai_dropdown'
|
||||
|
|
Loading…
Reference in New Issue
Block a user