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> = { 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(); 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( 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; }