Древовидные справочники: parent_id, drag&drop, древовидный выбор в полях table
This commit is contained in:
@@ -490,7 +490,7 @@ export function registerDataTableRoutes(app: import("express").Express): void {
|
||||
try {
|
||||
const tableId = parseInt(req.params.tableId);
|
||||
const table = await storage.getDataTable(tableId, req.organizationId!);
|
||||
|
||||
|
||||
if (!table) {
|
||||
return res.status(404).json({ error: 'Таблица не найдена' });
|
||||
}
|
||||
@@ -500,12 +500,12 @@ export function registerDataTableRoutes(app: import("express").Express): void {
|
||||
return res.status(403).json({ error: 'Нет прав для добавления строк' });
|
||||
}
|
||||
|
||||
const { values } = req.body;
|
||||
const { values, parentId } = req.body;
|
||||
if (!values || !Array.isArray(values)) {
|
||||
return res.status(400).json({ error: 'Значения обязательны' });
|
||||
}
|
||||
|
||||
const row = await storage.createDataTableRow({ tableId, values });
|
||||
const row = await storage.createDataTableRow({ tableId, values, parentId: parentId ?? null });
|
||||
res.status(201).json({ row });
|
||||
} catch (error) {
|
||||
console.error('Create table row error:', error);
|
||||
@@ -514,7 +514,7 @@ export function registerDataTableRoutes(app: import("express").Express): void {
|
||||
}
|
||||
);
|
||||
|
||||
// Update table row
|
||||
// Update table row (values and/or hierarchy)
|
||||
app.patch(['/api/tables/:tableId/rows/:rowId', '/api/directories/:tableId/rows/:rowId'],
|
||||
authenticateToken,
|
||||
tenantIsolation,
|
||||
@@ -523,7 +523,7 @@ export function registerDataTableRoutes(app: import("express").Express): void {
|
||||
const tableId = parseInt(req.params.tableId);
|
||||
const rowId = parseInt(req.params.rowId);
|
||||
const table = await storage.getDataTable(tableId, req.organizationId!);
|
||||
|
||||
|
||||
if (!table) {
|
||||
return res.status(404).json({ error: 'Таблица не найдена' });
|
||||
}
|
||||
@@ -533,16 +533,34 @@ export function registerDataTableRoutes(app: import("express").Express): void {
|
||||
return res.status(403).json({ error: 'Нет прав для редактирования строк' });
|
||||
}
|
||||
|
||||
const { values } = req.body;
|
||||
if (!values || !Array.isArray(values)) {
|
||||
return res.status(400).json({ error: 'Значения обязательны' });
|
||||
const { values, parentId, position } = req.body;
|
||||
let row;
|
||||
|
||||
if (parentId !== undefined || position !== undefined) {
|
||||
row = await storage.moveDataTableRow(
|
||||
rowId,
|
||||
tableId,
|
||||
req.organizationId!,
|
||||
parentId ?? null,
|
||||
position ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
if (values !== undefined) {
|
||||
if (!Array.isArray(values)) {
|
||||
return res.status(400).json({ error: 'Значения должны быть массивом' });
|
||||
}
|
||||
row = await storage.updateDataTableRow(rowId, tableId, req.organizationId!, { values });
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
return res.status(400).json({ error: 'Не переданы данные для обновления' });
|
||||
}
|
||||
|
||||
const row = await storage.updateDataTableRow(rowId, tableId, req.organizationId!, { values });
|
||||
res.json({ row });
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Update table row error:', error);
|
||||
res.status(500).json({ error: 'Ошибка обновления строки' });
|
||||
res.status(400).json({ error: error.message || 'Ошибка обновления строки' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -184,13 +184,102 @@ export class DataTablesCoreStorage extends BotsStorage {
|
||||
));
|
||||
if (!table) return;
|
||||
|
||||
await db
|
||||
.update(dataTableRows)
|
||||
.set({ isDeleted: true, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(dataTableRows.id, id),
|
||||
eq(dataTableRows.tableId, tableId)
|
||||
));
|
||||
await db.transaction(async (tx) => {
|
||||
// Move direct children to root before soft-deleting the row
|
||||
await tx
|
||||
.update(dataTableRows)
|
||||
.set({ parentId: null, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(dataTableRows.parentId, id),
|
||||
eq(dataTableRows.tableId, tableId)
|
||||
));
|
||||
|
||||
await tx
|
||||
.update(dataTableRows)
|
||||
.set({ isDeleted: true, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(dataTableRows.id, id),
|
||||
eq(dataTableRows.tableId, tableId)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
private async getRowsMap(tableId: number, organizationId: number): Promise<Map<number, DataTableRow>> {
|
||||
const rows = await this.getDataTableRows(tableId, organizationId);
|
||||
const map = new Map<number, DataTableRow>();
|
||||
for (const row of rows) {
|
||||
map.set(row.id, row);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private getDescendantIds(rowsById: Map<number, DataTableRow>, rootId: number): Set<number> {
|
||||
const ids = new Set<number>();
|
||||
const walk = (parentId: number) => {
|
||||
for (const row of rowsById.values()) {
|
||||
if (row.parentId === parentId) {
|
||||
ids.add(row.id);
|
||||
walk(row.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(rootId);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async moveDataTableRow(
|
||||
id: number,
|
||||
tableId: number,
|
||||
organizationId: number,
|
||||
parentId: number | null,
|
||||
position: number
|
||||
): Promise<DataTableRow> {
|
||||
const table = await this.getDataTable(tableId, organizationId);
|
||||
if (!table) throw new Error('Table not found');
|
||||
|
||||
const row = await this.getDataTableRow(id, tableId, organizationId);
|
||||
if (!row) throw new Error('Row not found');
|
||||
|
||||
const targetParentId = parentId ?? null;
|
||||
|
||||
if (targetParentId !== null) {
|
||||
if (targetParentId === id) {
|
||||
throw new Error('Cannot move row into itself');
|
||||
}
|
||||
const rowsById = await this.getRowsMap(tableId, organizationId);
|
||||
if (this.getDescendantIds(rowsById, id).has(targetParentId)) {
|
||||
throw new Error('Cannot move row into its descendant');
|
||||
}
|
||||
}
|
||||
|
||||
const rows = await this.getDataTableRows(tableId, organizationId);
|
||||
const siblings = rows
|
||||
.filter((r) => r.id !== id && (r.parentId ?? null) === targetParentId)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const clampedPosition = Math.max(0, Math.min(position, siblings.length));
|
||||
const reordered = [
|
||||
...siblings.slice(0, clampedPosition),
|
||||
row,
|
||||
...siblings.slice(clampedPosition),
|
||||
];
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
for (let i = 0; i < reordered.length; i++) {
|
||||
const updates: Partial<DataTableRow> = { position: i };
|
||||
if (reordered[i].id === id) {
|
||||
updates.parentId = targetParentId;
|
||||
}
|
||||
await tx
|
||||
.update(dataTableRows)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(eq(dataTableRows.id, reordered[i].id));
|
||||
}
|
||||
});
|
||||
|
||||
const movedRow = await this.getDataTableRow(id, tableId, organizationId);
|
||||
if (!movedRow) throw new Error('Row not found after move');
|
||||
return movedRow;
|
||||
}
|
||||
|
||||
async bulkCreateDataTableRows(tableId: number, rows: { values: string[] }[]): Promise<DataTableRow[]> {
|
||||
|
||||
Reference in New Issue
Block a user