testomatio-editor-blocks 0.4.31 → 0.4.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ export declare const fileBlock: {
2
+ config: {
3
+ type: "file";
4
+ propSchema: {
5
+ backgroundColor: {
6
+ default: "default";
7
+ };
8
+ name: {
9
+ default: "";
10
+ };
11
+ url: {
12
+ default: "";
13
+ };
14
+ caption: {
15
+ default: "";
16
+ };
17
+ };
18
+ content: "none";
19
+ isFileBlock: true;
20
+ };
21
+ implementation: import("@blocknote/core").TiptapBlockImplementation<{
22
+ type: "file";
23
+ propSchema: {
24
+ backgroundColor: {
25
+ default: "default";
26
+ };
27
+ name: {
28
+ default: "";
29
+ };
30
+ url: {
31
+ default: "";
32
+ };
33
+ caption: {
34
+ default: "";
35
+ };
36
+ };
37
+ content: "none";
38
+ isFileBlock: true;
39
+ }, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
40
+ };
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { fileBlockConfig, fileParse } from "@blocknote/core";
3
+ import { createReactBlockSpec } from "@blocknote/react";
4
+ import { useCallback } from "react";
5
+ import { resolveFileDisplayUrl } from "../fileDisplayUrl";
6
+ function FileIcon(props) {
7
+ const { caption, url } = props.block.props;
8
+ const iconUrl = caption || (url ? resolveFileDisplayUrl(url) : "");
9
+ if (iconUrl) {
10
+ return _jsx("img", { src: iconUrl, alt: "", width: 24, height: 24, style: { display: "block" } });
11
+ }
12
+ return (_jsx("svg", { width: 24, height: 24, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z" }) }));
13
+ }
14
+ function FileBlockRender(props) {
15
+ const addFileButtonMouseDownHandler = useCallback((event) => event.preventDefault(), []);
16
+ const addFileButtonClickHandler = useCallback(() => {
17
+ props.editor.transact((tr) => tr.setMeta(props.editor.filePanel.plugins[0], {
18
+ block: props.block,
19
+ }));
20
+ }, [props.block, props.editor]);
21
+ if (props.block.props.url === "") {
22
+ return (_jsx("div", { className: "bn-file-block-content-wrapper", children: _jsxs("div", { className: "bn-add-file-button", onMouseDown: addFileButtonMouseDownHandler, onClick: addFileButtonClickHandler, children: [_jsx("div", { className: "bn-add-file-button-icon", children: _jsx(FileIcon, { block: props.block }) }), _jsx("div", { className: "bn-add-file-button-text", children: "Add file" })] }) }));
23
+ }
24
+ return (_jsx("div", { className: "bn-file-block-content-wrapper", children: _jsxs("div", { className: "bn-file-name-with-icon", contentEditable: false, draggable: false, children: [_jsx("div", { className: "bn-file-icon", children: _jsx(FileIcon, { block: props.block }) }), _jsx("p", { className: "bn-file-name", children: props.block.props.name || props.block.props.url })] }) }));
25
+ }
26
+ export const fileBlock = createReactBlockSpec(fileBlockConfig, {
27
+ render: FileBlockRender,
28
+ parse: fileParse,
29
+ toExternalHTML: (props) => {
30
+ if (!props.block.props.url) {
31
+ return _jsx("p", { children: "Add file" });
32
+ }
33
+ return (_jsx("a", { href: props.block.props.url, children: props.block.props.name || props.block.props.url }));
34
+ },
35
+ });
@@ -270,8 +270,9 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
270
270
  case "file": {
271
271
  const url = block.props.url || "";
272
272
  const name = block.props.name || "";
273
+ const caption = block.props.caption || "";
273
274
  if (url) {
274
- const displayUrl = resolveFileDisplayUrl(name, url);
275
+ const displayUrl = caption || resolveFileDisplayUrl(url);
275
276
  lines.push(`[![${name}](${displayUrl})](${url})`);
276
277
  }
277
278
  return flattenWithBlankLine(lines, true);
@@ -1231,6 +1232,7 @@ export function markdownToBlocks(markdown) {
1231
1232
  props: {
1232
1233
  name: fileMatch[1] || "",
1233
1234
  url: fileMatch[3],
1235
+ caption: fileMatch[2] || "",
1234
1236
  },
1235
1237
  children: [],
1236
1238
  });
@@ -1,6 +1,46 @@
1
1
  import { BlockNoteSchema } from "@blocknote/core";
2
2
  import { htmlToMarkdown, markdownToHtml } from "./blocks/markdown";
3
3
  export declare const customSchema: BlockNoteSchema<import("@blocknote/core").BlockSchemaFromSpecs<{
4
+ file: {
5
+ config: {
6
+ type: "file";
7
+ propSchema: {
8
+ backgroundColor: {
9
+ default: "default";
10
+ };
11
+ name: {
12
+ default: "";
13
+ };
14
+ url: {
15
+ default: "";
16
+ };
17
+ caption: {
18
+ default: "";
19
+ };
20
+ };
21
+ content: "none";
22
+ isFileBlock: true;
23
+ };
24
+ implementation: import("@blocknote/core").TiptapBlockImplementation<{
25
+ type: "file";
26
+ propSchema: {
27
+ backgroundColor: {
28
+ default: "default";
29
+ };
30
+ name: {
31
+ default: "";
32
+ };
33
+ url: {
34
+ default: "";
35
+ };
36
+ caption: {
37
+ default: "";
38
+ };
39
+ };
40
+ content: "none";
41
+ isFileBlock: true;
42
+ }, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
43
+ };
4
44
  testStep: {
5
45
  config: {
6
46
  readonly type: "testStep";
@@ -343,46 +383,6 @@ export declare const customSchema: BlockNoteSchema<import("@blocknote/core").Blo
343
383
  };
344
384
  }, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
345
385
  };
346
- file: {
347
- config: {
348
- type: "file";
349
- propSchema: {
350
- backgroundColor: {
351
- default: "default";
352
- };
353
- name: {
354
- default: "";
355
- };
356
- url: {
357
- default: "";
358
- };
359
- caption: {
360
- default: "";
361
- };
362
- };
363
- content: "none";
364
- isFileBlock: true;
365
- };
366
- implementation: import("@blocknote/core").TiptapBlockImplementation<{
367
- type: "file";
368
- propSchema: {
369
- backgroundColor: {
370
- default: "default";
371
- };
372
- name: {
373
- default: "";
374
- };
375
- url: {
376
- default: "";
377
- };
378
- caption: {
379
- default: "";
380
- };
381
- };
382
- content: "none";
383
- isFileBlock: true;
384
- }, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
385
- };
386
386
  image: {
387
387
  config: {
388
388
  type: "image";
@@ -2,10 +2,12 @@ import { defaultBlockSpecs } from "@blocknote/core";
2
2
  import { BlockNoteSchema } from "@blocknote/core";
3
3
  import { stepBlock } from "./blocks/step";
4
4
  import { snippetBlock } from "./blocks/snippet";
5
+ import { fileBlock } from "./blocks/fileBlock";
5
6
  import { htmlToMarkdown, markdownToHtml } from "./blocks/markdown";
6
7
  export const customSchema = BlockNoteSchema.create({
7
8
  blockSpecs: {
8
9
  ...defaultBlockSpecs,
10
+ file: fileBlock,
9
11
  testStep: stepBlock,
10
12
  snippet: snippetBlock,
11
13
  },
@@ -1,3 +1,3 @@
1
- export type FileDisplayUrlResolver = (fileName: string) => string;
1
+ export type FileDisplayUrlResolver = (fileUrl: string) => string;
2
2
  export declare function setFileDisplayUrlResolver(fn: FileDisplayUrlResolver | null): void;
3
- export declare function resolveFileDisplayUrl(fileName: string, fallbackUrl: string): string;
3
+ export declare function resolveFileDisplayUrl(fileUrl: string): string;
@@ -2,14 +2,9 @@ let resolver = null;
2
2
  export function setFileDisplayUrlResolver(fn) {
3
3
  resolver = fn;
4
4
  }
5
- export function resolveFileDisplayUrl(fileName, fallbackUrl) {
6
- var _a;
5
+ export function resolveFileDisplayUrl(fileUrl) {
7
6
  if (resolver) {
8
- return resolver(fileName);
7
+ return resolver(fileUrl);
9
8
  }
10
- const ext = ((_a = fileName.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || "";
11
- if (ext) {
12
- return `/images/file-type-icons/${ext}.svg`;
13
- }
14
- return fallbackUrl;
9
+ return `/images/file-type-icons/file.svg`;
15
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",
@@ -0,0 +1,87 @@
1
+ import { fileBlockConfig, fileParse } from "@blocknote/core";
2
+ import { createReactBlockSpec } from "@blocknote/react";
3
+ import type { ReactCustomBlockRenderProps } from "@blocknote/react";
4
+ import { useCallback } from "react";
5
+ import { resolveFileDisplayUrl } from "../fileDisplayUrl";
6
+
7
+ function FileIcon(props: { block: { props: { caption?: string; url?: string } } }) {
8
+ const { caption, url } = props.block.props;
9
+ const iconUrl = caption || (url ? resolveFileDisplayUrl(url) : "");
10
+
11
+ if (iconUrl) {
12
+ return <img src={iconUrl} alt="" width={24} height={24} style={{ display: "block" }} />;
13
+ }
14
+
15
+ return (
16
+ <svg width={24} height={24} viewBox="0 0 24 24" fill="currentColor">
17
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z" />
18
+ </svg>
19
+ );
20
+ }
21
+
22
+ function FileBlockRender(
23
+ props: ReactCustomBlockRenderProps<typeof fileBlockConfig, any, any>,
24
+ ) {
25
+ const addFileButtonMouseDownHandler = useCallback(
26
+ (event: React.MouseEvent) => event.preventDefault(),
27
+ [],
28
+ );
29
+ const addFileButtonClickHandler = useCallback(() => {
30
+ props.editor.transact((tr: any) =>
31
+ tr.setMeta(props.editor.filePanel!.plugins[0], {
32
+ block: props.block,
33
+ }),
34
+ );
35
+ }, [props.block, props.editor]);
36
+
37
+ if (props.block.props.url === "") {
38
+ return (
39
+ <div
40
+ className="bn-file-block-content-wrapper"
41
+ >
42
+ <div
43
+ className="bn-add-file-button"
44
+ onMouseDown={addFileButtonMouseDownHandler}
45
+ onClick={addFileButtonClickHandler}
46
+ >
47
+ <div className="bn-add-file-button-icon">
48
+ <FileIcon block={props.block} />
49
+ </div>
50
+ <div className="bn-add-file-button-text">Add file</div>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <div className="bn-file-block-content-wrapper">
58
+ <div
59
+ className="bn-file-name-with-icon"
60
+ contentEditable={false}
61
+ draggable={false}
62
+ >
63
+ <div className="bn-file-icon">
64
+ <FileIcon block={props.block} />
65
+ </div>
66
+ <p className="bn-file-name">
67
+ {props.block.props.name || props.block.props.url}
68
+ </p>
69
+ </div>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ export const fileBlock = createReactBlockSpec(fileBlockConfig, {
75
+ render: FileBlockRender,
76
+ parse: fileParse,
77
+ toExternalHTML: (props) => {
78
+ if (!props.block.props.url) {
79
+ return <p>Add file</p>;
80
+ }
81
+ return (
82
+ <a href={props.block.props.url}>
83
+ {props.block.props.name || props.block.props.url}
84
+ </a>
85
+ );
86
+ },
87
+ });
@@ -1516,12 +1516,7 @@ describe("markdownToBlocks", () => {
1516
1516
  });
1517
1517
 
1518
1518
  describe("file block serialization", () => {
1519
- it("serializes a file block using display url resolver", () => {
1520
- setFileDisplayUrlResolver((name: string) => {
1521
- const ext = name.split(".").pop()?.toLowerCase() || "";
1522
- return `/images/file-type-icons/${ext}.svg`;
1523
- });
1524
-
1519
+ it("serializes a file block using caption as display url", () => {
1525
1520
  const blocks: CustomEditorBlock[] = [
1526
1521
  {
1527
1522
  id: "1",
@@ -1530,7 +1525,7 @@ describe("file block serialization", () => {
1530
1525
  ...baseProps,
1531
1526
  url: "https://example.com/file.pdf",
1532
1527
  name: "report.pdf",
1533
- caption: "",
1528
+ caption: "/images/file-type-icons/pdf.svg",
1534
1529
  },
1535
1530
  content: undefined as any,
1536
1531
  children: [],
@@ -1538,11 +1533,32 @@ describe("file block serialization", () => {
1538
1533
  ];
1539
1534
  const md = blocksToMarkdown(blocks);
1540
1535
  expect(md).toBe("[![report.pdf](/images/file-type-icons/pdf.svg)](https://example.com/file.pdf)");
1536
+ });
1541
1537
 
1542
- setFileDisplayUrlResolver(null);
1538
+ it("derives icon from url when no caption is set", () => {
1539
+ const blocks: CustomEditorBlock[] = [
1540
+ {
1541
+ id: "1",
1542
+ type: "file",
1543
+ props: {
1544
+ ...baseProps,
1545
+ url: "https://example.com/file.pdf",
1546
+ name: "",
1547
+ caption: "",
1548
+ },
1549
+ content: undefined as any,
1550
+ children: [],
1551
+ },
1552
+ ];
1553
+ const md = blocksToMarkdown(blocks);
1554
+ expect(md).toBe("[![](/images/file-type-icons/file.svg)](https://example.com/file.pdf)");
1543
1555
  });
1544
1556
 
1545
- it("falls back to url when no resolver is set", () => {
1557
+ it("uses custom resolver when set", () => {
1558
+ setFileDisplayUrlResolver(() => {
1559
+ return `/custom-icons/file.svg`;
1560
+ });
1561
+
1546
1562
  const blocks: CustomEditorBlock[] = [
1547
1563
  {
1548
1564
  id: "1",
@@ -1550,7 +1566,7 @@ describe("file block serialization", () => {
1550
1566
  props: {
1551
1567
  ...baseProps,
1552
1568
  url: "https://example.com/file.pdf",
1553
- name: "file.pdf",
1569
+ name: "",
1554
1570
  caption: "",
1555
1571
  },
1556
1572
  content: undefined as any,
@@ -1558,7 +1574,9 @@ describe("file block serialization", () => {
1558
1574
  },
1559
1575
  ];
1560
1576
  const md = blocksToMarkdown(blocks);
1561
- expect(md).toBe("[![file.pdf](/images/file-type-icons/pdf.svg)](https://example.com/file.pdf)");
1577
+ expect(md).toBe("[![](/custom-icons/file.svg)](https://example.com/file.pdf)");
1578
+
1579
+ setFileDisplayUrlResolver(null);
1562
1580
  });
1563
1581
 
1564
1582
  it("outputs nothing when url is empty", () => {
@@ -1582,22 +1600,24 @@ describe("file block serialization", () => {
1582
1600
  });
1583
1601
 
1584
1602
  describe("file block parsing", () => {
1585
- it("parses file markdown into a file block", () => {
1603
+ it("parses file markdown and stores display url in caption", () => {
1586
1604
  const markdown = "[![report.pdf](/images/file-type-icons/pdf.svg)](https://example.com/file.pdf)";
1587
1605
  const blocks = markdownToBlocks(markdown);
1588
1606
  expect(blocks).toHaveLength(1);
1589
1607
  expect(blocks[0].type).toBe("file");
1590
1608
  expect((blocks[0].props as any).name).toBe("report.pdf");
1591
1609
  expect((blocks[0].props as any).url).toBe("https://example.com/file.pdf");
1610
+ expect((blocks[0].props as any).caption).toBe("/images/file-type-icons/pdf.svg");
1592
1611
  });
1593
1612
 
1594
1613
  it("parses file markdown with empty name", () => {
1595
- const markdown = "[![](https://example.com/url)](https://example.com/url)";
1614
+ const markdown = "[![](/images/file-type-icons/json.svg)](https://example.com/file.json)";
1596
1615
  const blocks = markdownToBlocks(markdown);
1597
1616
  expect(blocks).toHaveLength(1);
1598
1617
  expect(blocks[0].type).toBe("file");
1599
1618
  expect((blocks[0].props as any).name).toBe("");
1600
- expect((blocks[0].props as any).url).toBe("https://example.com/url");
1619
+ expect((blocks[0].props as any).url).toBe("https://example.com/file.json");
1620
+ expect((blocks[0].props as any).caption).toBe("/images/file-type-icons/json.svg");
1601
1621
  });
1602
1622
 
1603
1623
  it("does not confuse file blocks with image blocks", () => {
@@ -1607,12 +1627,7 @@ describe("file block parsing", () => {
1607
1627
  expect(blocks[0].type).toBe("image");
1608
1628
  });
1609
1629
 
1610
- it("round-trips file blocks through serialize and parse", () => {
1611
- setFileDisplayUrlResolver((name: string) => {
1612
- const ext = name.split(".").pop()?.toLowerCase() || "";
1613
- return `/images/file-type-icons/${ext}.svg`;
1614
- });
1615
-
1630
+ it("round-trips file blocks preserving icon url in caption", () => {
1616
1631
  const blocks: CustomEditorBlock[] = [
1617
1632
  {
1618
1633
  id: "1",
@@ -1621,19 +1636,27 @@ describe("file block parsing", () => {
1621
1636
  ...baseProps,
1622
1637
  url: "https://example.com/doc.xlsx",
1623
1638
  name: "doc.xlsx",
1624
- caption: "",
1639
+ caption: "/images/file-type-icons/xlsx.svg",
1625
1640
  },
1626
1641
  content: undefined as any,
1627
1642
  children: [],
1628
1643
  },
1629
1644
  ];
1630
1645
  const md = blocksToMarkdown(blocks);
1646
+ expect(md).toBe("[![doc.xlsx](/images/file-type-icons/xlsx.svg)](https://example.com/doc.xlsx)");
1647
+
1631
1648
  const parsed = markdownToBlocks(md);
1632
1649
  expect(parsed).toHaveLength(1);
1633
1650
  expect(parsed[0].type).toBe("file");
1634
1651
  expect((parsed[0].props as any).url).toBe("https://example.com/doc.xlsx");
1635
1652
  expect((parsed[0].props as any).name).toBe("doc.xlsx");
1653
+ expect((parsed[0].props as any).caption).toBe("/images/file-type-icons/xlsx.svg");
1654
+ });
1636
1655
 
1637
- setFileDisplayUrlResolver(null);
1656
+ it("round-trips file blocks without name", () => {
1657
+ const markdown = "[![](/images/file-type-icons/json.svg)](https://example.com/data.json)";
1658
+ const parsed = markdownToBlocks(markdown);
1659
+ const md = blocksToMarkdown(parsed as CustomEditorBlock[]);
1660
+ expect(md).toBe(markdown);
1638
1661
  });
1639
1662
  });
@@ -348,8 +348,9 @@ function serializeBlock(
348
348
  case "file": {
349
349
  const url = (block.props as any).url || "";
350
350
  const name = (block.props as any).name || "";
351
+ const caption = (block.props as any).caption || "";
351
352
  if (url) {
352
- const displayUrl = resolveFileDisplayUrl(name, url);
353
+ const displayUrl = caption || resolveFileDisplayUrl(url);
353
354
  lines.push(`[![${name}](${displayUrl})](${url})`);
354
355
  }
355
356
  return flattenWithBlankLine(lines, true);
@@ -1473,6 +1474,7 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1473
1474
  props: {
1474
1475
  name: fileMatch[1] || "",
1475
1476
  url: fileMatch[3],
1477
+ caption: fileMatch[2] || "",
1476
1478
  },
1477
1479
  children: [],
1478
1480
  } as CustomPartialBlock);
@@ -2,11 +2,13 @@ import { defaultBlockSpecs } from "@blocknote/core";
2
2
  import { BlockNoteSchema } from "@blocknote/core";
3
3
  import { stepBlock } from "./blocks/step";
4
4
  import { snippetBlock } from "./blocks/snippet";
5
+ import { fileBlock } from "./blocks/fileBlock";
5
6
  import { htmlToMarkdown, markdownToHtml } from "./blocks/markdown";
6
7
 
7
8
  export const customSchema = BlockNoteSchema.create({
8
9
  blockSpecs: {
9
10
  ...defaultBlockSpecs,
11
+ file: fileBlock,
10
12
  testStep: stepBlock,
11
13
  snippet: snippetBlock,
12
14
  },
@@ -1,4 +1,4 @@
1
- export type FileDisplayUrlResolver = (fileName: string) => string;
1
+ export type FileDisplayUrlResolver = (fileUrl: string) => string;
2
2
 
3
3
  let resolver: FileDisplayUrlResolver | null = null;
4
4
 
@@ -6,13 +6,9 @@ export function setFileDisplayUrlResolver(fn: FileDisplayUrlResolver | null) {
6
6
  resolver = fn;
7
7
  }
8
8
 
9
- export function resolveFileDisplayUrl(fileName: string, fallbackUrl: string): string {
9
+ export function resolveFileDisplayUrl(fileUrl: string): string {
10
10
  if (resolver) {
11
- return resolver(fileName);
11
+ return resolver(fileUrl);
12
12
  }
13
- const ext = fileName.split(".").pop()?.toLowerCase() || "";
14
- if (ext) {
15
- return `/images/file-type-icons/${ext}.svg`;
16
- }
17
- return fallbackUrl;
13
+ return `/images/file-type-icons/file.svg`;
18
14
  }