mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
editor: add options to export/import table <> csv
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
@@ -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") {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
6666
packages/editor/package-lock.json
generated
6666
packages/editor/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -350,6 +350,11 @@ const tools: Record<ToolId, ToolDefinition> = {
|
||||
icon: "indent",
|
||||
title: strings.indent(),
|
||||
conditional: true
|
||||
},
|
||||
exportToCSV: {
|
||||
icon: "csv",
|
||||
title: strings.exportCsv(),
|
||||
conditional: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user