editor: add options to export/import table <> csv

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2026-01-07 11:43:10 +05:00
committed by Abdullah Atta
parent 210549445e
commit 2cf7f0cc27
15 changed files with 6709 additions and 147 deletions

View File

@@ -194,7 +194,9 @@ function TipTap(props: TipTapProps) {
const features = useAreFeaturesAvailable([
"callout",
"outlineList",
"taskList"
"taskList",
"exportTableAsCsv",
"importCsvToTable"
]);
usePermissionHandler({
@@ -202,7 +204,9 @@ function TipTap(props: TipTapProps) {
callout: !!features?.callout?.isAllowed,
outlineList: !!features?.outlineList?.isAllowed,
taskList: !!features?.taskList?.isAllowed,
insertAttachment: !!useUserStore.getState().isLoggedIn
insertAttachment: !!useUserStore.getState().isLoggedIn,
exportTableAsCsv: !!features?.exportTableAsCsv?.isAllowed,
importCsvToTable: !!features?.importCsvToTable?.isAllowed
},
onPermissionDenied: (claim, silent) => {
if (claim === "insertAttachment") {

View File

@@ -491,6 +491,28 @@ const features = {
believer: createLimit(true),
legacyPro: createLimit(true)
}
}),
exportTableAsCsv: createFeature({
id: "exportTableAsCsv",
title: "Export table as CSV",
availability: {
free: createLimit(false),
essential: createLimit(true),
pro: createLimit(true),
believer: createLimit(true),
legacyPro: createLimit(true)
}
}),
importCsvToTable: createFeature({
id: "importCsvToTable",
title: "Import CSV to table",
availability: {
free: createLimit(false),
essential: createLimit(true),
pro: createLimit(true),
believer: createLimit(true),
legacyPro: createLimit(true)
}
})
};

File diff suppressed because it is too large Load Diff

View File

@@ -66,9 +66,11 @@
"colord": "^2.9.3",
"detect-indent": "^7.0.1",
"entities": "5.0.0",
"file-saver": "^2.0.5",
"katex": "0.16.11",
"linkifyjs": "^4.1.3",
"nanoid": "5.0.7",
"papaparse": "^5.5.3",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-view": "1.34.2",
@@ -85,7 +87,9 @@
"@mdi/js": "7.4.47",
"@theme-ui/components": "0.16.1",
"@theme-ui/core": "0.16.1",
"@types/file-saver": "^2.0.7",
"@types/katex": "0.16.7",
"@types/papaparse": "^5.5.2",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.5",
"@types/react-color": "^3.0.12",

View File

@@ -21,6 +21,9 @@ import { Editor } from "@tiptap/core";
import { EditorState, TextSelection, Transaction } from "prosemirror-state";
import { Node } from "prosemirror-model";
import { selectedRect, TableRect } from "./prosemirror-tables/commands.js";
import { saveAs } from "file-saver";
import { unparse, parse } from "papaparse";
import { hasPermission } from "../../types.js";
type TableCell = {
cell: Node;
@@ -189,11 +192,70 @@ function selectColumn(
return true;
}
function escapeCell(cell: string): string {
return (cell || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function importCsvToTable(csvText: string, editor: Editor) {
const result = parse(csvText, {
skipEmptyLines: true
});
if (!result.data || result.data.length === 0) return "";
const rows = result.data as string[][];
let tableHTML = "<table>";
if (rows.length > 0) {
tableHTML += "<thead><tr>";
for (const cell of rows[0]) {
tableHTML += `<th><p>${escapeCell(cell)}</p></th>`;
}
tableHTML += "</tr></thead>";
}
tableHTML += "<tbody>";
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
tableHTML += "<tr>";
for (const cell of row) {
tableHTML += `<td><p>${escapeCell(cell)}</p></td>`;
}
tableHTML += "</tr>";
}
tableHTML += "</tbody></table>";
editor.chain().focus().insertContent(tableHTML).run();
}
function exportToCSV(editor?: Editor) {
if (!hasPermission("exportTableAsCsv")) return;
if (!editor) return;
const rect = selectedRect(editor.state);
const rows: string[][] = [];
rect.table.forEach((node) => {
const row: string[] = [];
node.forEach((cell) => row.push(cell.textContent));
rows.push(row);
});
saveAs(new Blob([new TextEncoder().encode(unparse(rows))]), "table.csv");
}
export {
moveColumnLeft,
moveColumnRight,
moveRowDown,
moveRowUp,
selectRow,
selectColumn
selectColumn,
exportToCSV,
escapeCell,
importCsvToTable
};

View File

@@ -244,6 +244,16 @@ declare module "@tiptap/core" {
anchorCell: number;
headCell?: number;
}) => ReturnType;
/**
* Export the current table as a CSV file
*/
exportTableAsCsv: () => ReturnType;
/**
* Import a CSV file into a new table at the current position
*/
importCsvToTable: () => ReturnType;
};
}

View File

@@ -31,7 +31,9 @@ const ClaimsMap = {
callout: ["setCallout"] as (keyof UnionCommands)[],
outlineList: ["toggleOutlineList"] as (keyof UnionCommands)[],
taskList: ["toggleTaskList"] as (keyof UnionCommands)[],
insertAttachment: ["insertAttachment"] as (keyof UnionCommands)[]
insertAttachment: ["insertAttachment"] as (keyof UnionCommands)[],
exportTableAsCsv: ["exportTableAsCsv"] as (keyof UnionCommands)[],
importCsvToTable: ["importCsvToTable"] as (keyof UnionCommands)[]
};
export function usePermissionHandler(options: PermissionHandlerOptions) {

View File

@@ -122,7 +122,8 @@ import {
mdiCheckboxMultipleMarked,
mdiMessageOutline,
mdiVectorLink,
mdiPinOutline
mdiPinOutline,
mdiFileDelimitedOutline
} from "@mdi/js";
export const Icons = {
@@ -235,6 +236,7 @@ export const Icons = {
heading: mdiFormatHeaderPound,
indent: mdiFormatIndentIncrease,
outdent: mdiFormatIndentDecrease,
csv: mdiFileDelimitedOutline,
plus: mdiPlus,
minus: mdiMinus,

View File

@@ -350,6 +350,11 @@ const tools: Record<ToolId, ToolDefinition> = {
icon: "indent",
title: strings.indent(),
conditional: true
},
exportToCSV: {
icon: "csv",
title: strings.exportCsv(),
conditional: true
}
};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ToolProps } from "../types.js";
import { Editor } from "../../types.js";
import { Editor, hasPermission } from "../../types.js";
import { Icons } from "../icons.js";
import { useMemo, useRef, useState } from "react";
import { EmbedPopup } from "../popups/embed-popup.js";
@@ -31,6 +31,7 @@ import { ImageUploadPopup } from "../popups/image-upload.js";
import { Button } from "../../components/button.js";
import { strings } from "@notesnook/intl";
import { keybindings } from "@notesnook/common";
import { importCsvToTable } from "../../extensions/table/actions.js";
export function InsertBlock(props: ToolProps) {
const { editor } = props;
@@ -205,6 +206,36 @@ const table = (editor: Editor): MenuItem => ({
menu: {
title: strings.insertTable(),
items: [
{
key: "import-csv",
type: "button",
title: strings.importCsv(),
icon: Icons.csv,
onClick: async () => {
if (!hasPermission("importCsvToTable")) return;
const input = document.createElement("input");
input.type = "file";
input.accept = ".csv,text/csv";
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
try {
const text = await file.text();
importCsvToTable(text, editor);
} catch (error) {
console.error("Error importing CSV:", error);
}
};
input.click();
}
},
{
key: "sep",
type: "separator"
},
{
key: "table-size-selector",
type: "popup",

View File

@@ -65,7 +65,8 @@ import {
CellBackgroundColor,
CellBorderColor,
CellTextColor,
CellBorderWidth
CellBorderWidth,
ExportToCSV
} from "./table.js";
import {
ImageSettings,
@@ -177,6 +178,7 @@ const tools = {
moveRowDown: MoveRowDown,
deleteRow: DeleteRow,
deleteTable: DeleteTable,
exportToCSV: ExportToCSV,
outdent: Outdent,
indent: Indent,

View File

@@ -29,7 +29,8 @@ import {
moveRowDown as moveRowDownAction,
moveRowUp as moveRowUpAction,
selectColumn,
selectRow
selectRow,
exportToCSV as exportToCsvAction
} from "../../extensions/table/actions.js";
import { MoreTools } from "../components/more-tools.js";
import { menuButtonToTool, toolToMenuButton } from "./utils.js";
@@ -173,7 +174,8 @@ export function TableProperties(props: ToolProps) {
splitCells(editor),
cellProperties(editor),
{ type: "separator", key: "tableSeperator" },
deleteTable(editor)
deleteTable(editor),
exportToCSV(editor)
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
@@ -412,6 +414,11 @@ const cellProperties = (editor: Editor): MenuButtonItem => ({
}
});
const exportToCSV = (editor: Editor): MenuButtonItem => ({
...toolToMenuButton(getToolDefinition("exportToCSV")),
onClick: () => exportToCsvAction(editor)
});
export const InsertColumnLeft = menuButtonToTool(insertColumnLeft);
export const InsertColumnRight = menuButtonToTool(insertColumnRight);
export const MoveColumnLeft = menuButtonToTool(moveColumnLeft);
@@ -425,3 +432,4 @@ export const MoveRowUp = menuButtonToTool(moveRowUp);
export const MoveRowDown = menuButtonToTool(moveRowDown);
export const DeleteRow = menuButtonToTool(deleteRow);
export const DeleteTable = menuButtonToTool(deleteTable);
export const ExportToCSV = menuButtonToTool(exportToCSV);

View File

@@ -2729,6 +2729,10 @@ msgstr "Export all notes as pdf, markdown, html or text in a single zip file"
msgid "Export as{0}"
msgstr "Export as{0}"
#: src/strings.ts:2623
msgid "Export CSV"
msgstr "Export CSV"
#: src/strings.ts:1385
msgid "Export notes as PDF, Markdown and HTML with Notesnook Pro"
msgstr "Export notes as PDF, Markdown and HTML with Notesnook Pro"
@@ -3340,6 +3344,10 @@ msgstr "Import & export"
msgid "Import completed"
msgstr "Import completed"
#: src/strings.ts:2624
msgid "Import CSV"
msgstr "Import CSV"
#: src/strings.ts:1766
msgid "import guide"
msgstr "import guide"

View File

@@ -2718,6 +2718,10 @@ msgstr ""
msgid "Export as{0}"
msgstr ""
#: src/strings.ts:2623
msgid "Export CSV"
msgstr ""
#: src/strings.ts:1385
msgid "Export notes as PDF, Markdown and HTML with Notesnook Pro"
msgstr ""
@@ -3320,6 +3324,10 @@ msgstr ""
msgid "Import completed"
msgstr ""
#: src/strings.ts:2624
msgid "Import CSV"
msgstr ""
#: src/strings.ts:1766
msgid "import guide"
msgstr ""

View File

@@ -2629,5 +2629,7 @@ Use this if changes from other devices are not appearing on this device. This wi
t`The incoming note could not be unlocked with the provided password. Enter the correct password for the incoming note`,
setExpiry: () => t`Set expiry`,
unsetExpiry: () => t`Unset expiry`,
expiryDate: () => t`Expiry date`
expiryDate: () => t`Expiry date`,
exportCsv: () => t`Export CSV`,
importCsv: () => t`Import CSV`
};