feat(fields): мягкое удаление полей формы — архив удалённых с восстановлением и блок «Удалённые поля» в задаче

This commit is contained in:
2026-09-21 14:35:45 +03:00
parent d5f2925886
commit e3f0ebfb89
18 changed files with 296 additions and 48 deletions

View File

@@ -1,6 +1,6 @@
import { forms, formTabs, formFields, formStatuses, statusTransitions, tasks, bookmarks, type Form, type FormTab, type FormField, type FormStatus, type StatusTransition, type InsertForm, type InsertFormTab, type InsertFormField, type InsertFormStatus, type InsertStatusTransition } from "@shared/schema";
import { db } from "../db";
import { eq, and, desc, exists, sql } from "drizzle-orm";
import { eq, and, desc, exists, sql, isNull } from "drizzle-orm";
import crypto from "crypto";
import { UserProfileStorage } from "./user-profile.storage";
import { invalidateAccessibleTasksForOrg } from "../utils/cache";
@@ -197,7 +197,7 @@ export class FormsStorage extends UserProfileStorage {
.select()
.from(formFields)
.innerJoin(forms, eq(formFields.formId, forms.id))
.where(and(eq(formFields.formId, formId), eq(forms.organizationId, organizationId)))
.where(and(eq(formFields.formId, formId), eq(forms.organizationId, organizationId), isNull(formFields.deletedAt)))
.orderBy(formFields.displayOrder)
.then(results => results.map(r => r.form_fields));
}
@@ -207,11 +207,53 @@ export class FormsStorage extends UserProfileStorage {
.select()
.from(formFields)
.innerJoin(forms, eq(formFields.formId, forms.id))
.where(and(eq(formFields.formId, formId), eq(forms.organizationId, organizationId)))
.where(and(eq(formFields.formId, formId), eq(forms.organizationId, organizationId), isNull(formFields.deletedAt)))
.orderBy(formFields.id)
.then(results => results.map(r => r.form_fields));
}
// Архив удалённых (мягко) полей формы — для восстановления и блока «Удалённые поля» в задаче
async getDeletedFormFields(formId: number, organizationId: number): Promise<FormField[]> {
return await db
.select()
.from(formFields)
.innerJoin(forms, eq(formFields.formId, forms.id))
.where(and(eq(formFields.formId, formId), eq(forms.organizationId, organizationId), sql`${formFields.deletedAt} IS NOT NULL`))
.orderBy(desc(formFields.deletedAt))
.then(results => results.map(r => r.form_fields));
}
// Восстановление удалённого поля. Код проверяем на конфликт с активным полем формы.
async restoreFormField(id: number, formId: number, organizationId: number): Promise<FormField> {
const [field] = await db
.select()
.from(formFields)
.innerJoin(forms, eq(formFields.formId, forms.id))
.where(and(eq(formFields.id, id), eq(formFields.formId, formId), eq(forms.organizationId, organizationId)));
if (!field?.form_fields) throw new Error(`Поле ${id} не найдено`);
if (!field.form_fields.deletedAt) throw new Error(`Поле "${field.form_fields.name}" не удалено`);
const [conflict] = await db
.select({ id: formFields.id, name: formFields.name })
.from(formFields)
.where(and(
eq(formFields.formId, formId),
eq(formFields.code, field.form_fields.code),
isNull(formFields.deletedAt),
sql`${formFields.id} <> ${id}`,
));
if (conflict) {
throw new Error(`Активное поле с кодом "${field.form_fields.code}" уже существует («${conflict.name}»). Переименуйте его или удалите.`);
}
const [restored] = await db
.update(formFields)
.set({ deletedAt: null })
.where(eq(formFields.id, id))
.returning();
return restored;
}
async getFormFieldById(id: number, organizationId: number): Promise<FormField | undefined> {
const [field] = await db
.select({ field: formFields })
@@ -243,11 +285,14 @@ export class FormsStorage extends UserProfileStorage {
}
async deleteFormField(id: number, formId: number, organizationId: number): Promise<void> {
// Мягкое удаление: значения в задачах сохраняются, поле доступно в архиве
await db
.delete(formFields)
.update(formFields)
.set({ deletedAt: new Date() })
.where(and(
eq(formFields.id, id),
eq(formFields.formId, formId),
isNull(formFields.deletedAt),
// Убедимся что form принадлежит организации через subquery
exists(db.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
));
@@ -290,9 +335,11 @@ export class FormsStorage extends UserProfileStorage {
): Promise<FormField[]> {
return await db.transaction(async (tx) => {
for (const fieldId of fieldIdsToDelete) {
// Мягкое удаление: значения в задачах сохраняются, поле уходит в архив
await tx
.delete(formFields)
.where(and(eq(formFields.id, fieldId), eq(formFields.formId, formId)));
.update(formFields)
.set({ deletedAt: new Date() })
.where(and(eq(formFields.id, fieldId), eq(formFields.formId, formId), isNull(formFields.deletedAt)));
}
const resultFields: FormField[] = [];