Files
buzz-sheet/lib/guides/extra-sheet.ts
T
gunshiz 75e3e22e9b
CI / Verify (push) Successful in 1m12s
CI / Build immutable images and deploy (push) Successful in 1m53s
fix : update
2026-09-05 18:45:57 +07:00

228 lines
7.6 KiB
TypeScript

import * as z from "zod";
import { normalizeExtraRichText } from "@/lib/rich-text";
const extraSheetId = z.string().min(1).max(128);
export const extraSheetSchema = z.strictObject({
columns: z.array(z.strictObject({
id: extraSheetId,
label: z.string().trim().min(1).max(120),
})).min(1),
rows: z.array(z.strictObject({
id: extraSheetId,
cells: z.record(z.string(), z.string().max(10_000)),
backgrounds: z.record(z.string(), z.string().regex(/^#[0-9A-Fa-f]{6}$/)).default({}),
})).min(1),
}).superRefine((sheet, context) => {
const columnIds = new Set(sheet.columns.map((column) => column.id));
if (columnIds.size !== sheet.columns.length) {
context.addIssue({ code: "custom", path: ["columns"], message: "รหัสคอลัมน์ต้องไม่ซ้ำกัน" });
}
const columnLabels = new Set(sheet.columns.map((column) => column.label.toLocaleLowerCase()));
if (columnLabels.size !== sheet.columns.length) {
context.addIssue({ code: "custom", path: ["columns"], message: "ชื่อคอลัมน์ต้องไม่ซ้ำกัน" });
}
const rowIds = new Set(sheet.rows.map((row) => row.id));
if (rowIds.size !== sheet.rows.length) {
context.addIssue({ code: "custom", path: ["rows"], message: "รหัสแถวต้องไม่ซ้ำกัน" });
}
sheet.rows.forEach((row, rowIndex) => {
[...Object.keys(row.cells), ...Object.keys(row.backgrounds ?? {})].forEach((columnId) => {
if (!columnIds.has(columnId)) {
context.addIssue({
code: "custom",
path: ["rows", rowIndex, "cells", columnId],
message: "ไม่พบคอลัมน์ของเซลล์นี้",
});
}
});
});
});
export type ExtraSheet = z.output<typeof extraSheetSchema>;
export type ExtraSheetCell = { value: string; backgroundColor?: string };
export type ExtraSheetMatrix = Array<Array<ExtraSheetCell | undefined>>;
type SheetPoint = { row: number; column: number };
export function nextExtraSheetColumnLabel(columns: ExtraSheet["columns"]): string {
const labels = new Set(columns.map((column) => column.label.toLocaleLowerCase()));
let index = columns.length + 1;
while (labels.has(`คอลัมน์ ${index}`.toLocaleLowerCase())) index += 1;
return `คอลัมน์ ${index}`;
}
export function createExtraSheet(createId: (prefix: string) => string): ExtraSheet {
const columns = [1, 2].map((index) => ({
id: createId("extra-sheet-column"),
label: `คอลัมน์ ${index}`,
}));
return {
columns,
rows: [1, 2].map(() => ({
id: createId("extra-sheet-row"),
cells: Object.fromEntries(columns.map((column) => [column.id, ""])),
backgrounds: {},
})),
};
}
export function extraSheetToMatrix(sheet: ExtraSheet): ExtraSheetMatrix {
return sheet.rows.map((row) =>
sheet.columns.map((column) => ({
value: row.cells[column.id] ?? "",
backgroundColor: row.backgrounds?.[column.id],
})),
);
}
export function matrixToExtraSheet(
sheet: ExtraSheet,
matrix: ExtraSheetMatrix,
createId: (prefix: string) => string,
): ExtraSheet {
const columnCount = Math.max(
sheet.columns.length,
...matrix.map((row) => row.length),
);
const columns = [...sheet.columns];
while (columns.length < columnCount) {
columns.push({
id: createId("extra-sheet-column"),
label: nextExtraSheetColumnLabel(columns),
});
}
const rowCount = Math.max(sheet.rows.length, matrix.length);
const rows = Array.from({ length: rowCount }, (_, rowIndex) => ({
id: sheet.rows[rowIndex]?.id ?? createId("extra-sheet-row"),
cells: Object.fromEntries(columns.map((column, columnIndex) => [
column.id,
String(matrix[rowIndex]?.[columnIndex]?.value ?? ""),
])),
backgrounds: Object.fromEntries(columns.flatMap((column, columnIndex) => {
const backgroundColor = matrix[rowIndex]?.[columnIndex]?.backgroundColor
?? sheet.rows[rowIndex]?.backgrounds?.[column.id];
return backgroundColor ? [[column.id, backgroundColor]] : [];
})),
}));
return { columns, rows };
}
export function addExtraSheetColumn(
sheet: ExtraSheet,
label: string,
id: string,
): ExtraSheet {
return {
columns: [...sheet.columns, { id, label }],
rows: sheet.rows.map((row) => ({ ...row, cells: { ...row.cells, [id]: "" } })),
};
}
export function addExtraSheetRow(sheet: ExtraSheet, id: string): ExtraSheet {
return {
...sheet,
rows: [...sheet.rows, {
id,
cells: Object.fromEntries(sheet.columns.map((column) => [column.id, ""])),
backgrounds: {},
}],
};
}
export function moveExtraSheetColumn(
sheet: ExtraSheet,
from: number,
to: number,
): ExtraSheet {
if (to < 0 || to >= sheet.columns.length || from === to) return sheet;
const columns = [...sheet.columns];
const [column] = columns.splice(from, 1);
columns.splice(to, 0, column);
return { ...sheet, columns };
}
export function moveExtraSheetRow(
sheet: ExtraSheet,
from: number,
to: number,
): ExtraSheet {
if (to < 0 || to >= sheet.rows.length || from === to) return sheet;
const rows = [...sheet.rows];
const [row] = rows.splice(from, 1);
rows.splice(to, 0, row);
return { ...sheet, rows };
}
export function deleteExtraSheetColumn(sheet: ExtraSheet, index: number): ExtraSheet {
if (sheet.columns.length === 1) return sheet;
const columnId = sheet.columns[index]?.id;
if (!columnId) return sheet;
return {
columns: sheet.columns.filter((_, columnIndex) => columnIndex !== index),
rows: sheet.rows.map((row) => ({
...row,
cells: Object.fromEntries(Object.entries(row.cells).filter(([key]) => key !== columnId)),
backgrounds: Object.fromEntries(Object.entries(row.backgrounds ?? {}).filter(([key]) => key !== columnId)),
})),
};
}
export function deleteExtraSheetRow(sheet: ExtraSheet, index: number): ExtraSheet {
if (sheet.rows.length === 1) return sheet;
return { ...sheet, rows: sheet.rows.filter((_, rowIndex) => rowIndex !== index) };
}
export function updateExtraSheetCell(
sheet: ExtraSheet,
rowIndex: number,
columnIndex: number,
value: string,
): ExtraSheet {
const row = sheet.rows[rowIndex];
const column = sheet.columns[columnIndex];
if (!row || !column) return sheet;
return {
...sheet,
rows: sheet.rows.map((candidate, index) => index === rowIndex
? { ...candidate, cells: { ...candidate.cells, [column.id]: value } }
: candidate),
};
}
export function updateExtraSheetCells(
sheet: ExtraSheet,
points: Iterable<SheetPoint>,
update: { value?: string; backgroundColor?: string | null },
): ExtraSheet {
const selected = new Set(Array.from(points, ({ row, column }) => `${row}:${column}`));
return {
...sheet,
rows: sheet.rows.map((row, rowIndex) => {
const cells = { ...row.cells };
const backgrounds = { ...(row.backgrounds ?? {}) };
sheet.columns.forEach((column, columnIndex) => {
if (!selected.has(`${rowIndex}:${columnIndex}`)) return;
if (update.value !== undefined) cells[column.id] = update.value;
if (update.backgroundColor === null) delete backgrounds[column.id];
else if (update.backgroundColor !== undefined) backgrounds[column.id] = update.backgroundColor;
});
return { ...row, cells, backgrounds };
}),
};
}
export function normalizeExtraSheetRichText(sheet: ExtraSheet): ExtraSheet {
return {
...sheet,
rows: sheet.rows.map((row) => ({
...row,
cells: Object.fromEntries(Object.entries(row.cells).map(([columnId, value]) => [
columnId,
normalizeExtraRichText(value),
])),
})),
};
}