feat(fields): мягкое удаление полей формы — архив удалённых с восстановлением и блок «Удалённые поля» в задаче
This commit is contained in:
@@ -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[] = [];
|
||||
|
||||
@@ -21,6 +21,8 @@ export class TasksStorage extends TasksCoreStorage {
|
||||
users: Array<SafeUser & { fullName: string }>;
|
||||
hasRelations: boolean;
|
||||
assignees: Array<TaskAssignee & { user: { id: number; firstName: string | null; lastName: string | null; email: string } }>;
|
||||
/** Удалённые (мягко) поля формы со значениями в fieldValues — блок «Удалённые поля» */
|
||||
deletedFields: FormField[];
|
||||
} | null> {
|
||||
const taskResult = await db
|
||||
.select()
|
||||
@@ -34,7 +36,7 @@ export class TasksStorage extends TasksCoreStorage {
|
||||
const task = result.tasks;
|
||||
const form = result.forms;
|
||||
|
||||
const [fields, statuses, tabs, transitions, fieldValues, subtasks, messages, orgUsers, orgRoles, relationsCheck, assignees] = await Promise.all([
|
||||
const [fields, statuses, tabs, transitions, fieldValues, subtasks, messages, orgUsers, orgRoles, relationsCheck, assignees, deletedFields] = await Promise.all([
|
||||
this.getFormFields(form.id, organizationId),
|
||||
this.getFormStatuses(form.id, organizationId),
|
||||
this.getFormTabs(form.id, organizationId),
|
||||
@@ -51,6 +53,7 @@ export class TasksStorage extends TasksCoreStorage {
|
||||
))
|
||||
.limit(1),
|
||||
this.getTaskAssignees(task.id, organizationId),
|
||||
this.getDeletedFormFields(form.id, organizationId),
|
||||
]);
|
||||
|
||||
const usersWithFullName = orgUsers.map((u: SafeUser) => ({
|
||||
@@ -235,6 +238,7 @@ export class TasksStorage extends TasksCoreStorage {
|
||||
users: usersWithFullName,
|
||||
hasRelations: relationsCheck.length > 0,
|
||||
assignees,
|
||||
deletedFields,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,8 @@ export class UsersStorage {
|
||||
and(
|
||||
eq(forms.organizationId, organizationId),
|
||||
eq(formFields.type, 'user'),
|
||||
eq(formFields.linkToUserProfile, true)
|
||||
eq(formFields.linkToUserProfile, true),
|
||||
isNull(formFields.deletedAt)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user