2025-07-26 09:29:27 +03:00

157 lines
4.5 KiB
TypeScript

import { RelationEditor } from "./relation_editor.js";
import { MonospaceFormatter, NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js";
import type { ColumnDefinition } from "tabulator-tables";
import { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
type ColumnType = LabelType | "relation";
export interface AttributeDefinitionInformation {
name: string;
title?: string;
type?: ColumnType;
}
const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = {
text: {
editor: "input"
},
boolean: {
formatter: "tickCross",
editor: "tickCross"
},
date: {
editor: "date",
},
datetime: {
editor: "datetime"
},
number: {
editor: "number"
},
time: {
editor: "input"
},
url: {
formatter: "link",
editor: "input"
},
color: {
editor: "input",
formatter: "color",
editorParams: {
elementAttributes: {
type: "color"
}
}
},
relation: {
editor: RelationEditor,
formatter: NoteFormatter
}
};
interface BuildColumnArgs {
info: AttributeDefinitionInformation[];
movableRows: boolean;
existingColumnData: ColumnDefinition[] | undefined;
rowNumberHint: number;
position?: number;
}
export function buildColumnDefinitions({ info, movableRows, existingColumnData, rowNumberHint, position }: BuildColumnArgs) {
let columnDefs: ColumnDefinition[] = [
{
title: "#",
headerSort: false,
hozAlign: "center",
resizable: false,
frozen: true,
rowHandle: movableRows,
width: calculateIndexColumnWidth(rowNumberHint, movableRows),
formatter: RowNumberFormatter(movableRows)
},
{
field: "noteId",
title: "Note ID",
formatter: MonospaceFormatter,
visible: false
},
{
field: "title",
title: "Title",
editor: "input",
formatter: NoteTitleFormatter,
width: 400
}
];
const seenFields = new Set<string>();
for (const { name, title, type } of info) {
const prefix = (type === "relation" ? "relations" : "labels");
const field = `${prefix}.${name}`;
if (seenFields.has(field)) {
continue;
}
columnDefs.push({
field,
title: title ?? name,
editor: "input",
rowHandle: false,
...labelTypeMappings[type ?? "text"],
});
seenFields.add(field);
}
if (existingColumnData) {
columnDefs = restoreExistingData(columnDefs, existingColumnData, position);
}
return columnDefs;
}
export function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) {
// 1. Keep existing columns, but restore their properties like width, visibility and order.
const newItemsByField = new Map<string, ColumnDefinition>(
newDefs.map(def => [def.field!, def])
);
const existingColumns = oldDefs
.filter(item => (item.field && newItemsByField.has(item.field!)) || item.title === "#")
.map(oldItem => {
const data = newItemsByField.get(oldItem.field!)!;
if (oldItem.resizable !== false && oldItem.width !== undefined) {
data.width = oldItem.width;
}
if (oldItem.visible !== undefined) {
data.visible = oldItem.visible;
}
return data;
}) as ColumnDefinition[];
// 2. Determine new columns.
const existingFields = new Set(existingColumns.map(item => item.field));
const newColumns = newDefs
.filter(item => !existingFields.has(item.field!));
// Clamp position to a valid range
const insertPos = position !== undefined
? Math.min(Math.max(position, 0), existingColumns.length)
: existingColumns.length;
// 3. Insert new columns at the specified position
return [
...existingColumns.slice(0, insertPos),
...newColumns,
...existingColumns.slice(insertPos)
];
}
function calculateIndexColumnWidth(rowNumberHint: number, movableRows: boolean): number {
let columnWidth = 16 * (rowNumberHint.toString().length || 1);
if (movableRows) {
columnWidth += 32;
}
return columnWidth;
}