testomatio-editor-blocks 0.4.30 → 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
+ });
@@ -1,4 +1,5 @@
1
1
  import { isLinkInlineContent, isStyledTextInlineContent, } from "@blocknote/core";
2
+ import { resolveFileDisplayUrl } from "./fileDisplayUrl";
2
3
  import { isStepsHeading } from "./blocks/step";
3
4
  const BASE_BLOCK_PROPS = {
4
5
  textAlignment: "left",
@@ -271,7 +272,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
271
272
  const name = block.props.name || "";
272
273
  const caption = block.props.caption || "";
273
274
  if (url) {
274
- const displayUrl = caption || url;
275
+ const displayUrl = caption || resolveFileDisplayUrl(url);
275
276
  lines.push(`[![${name}](${displayUrl})](${url})`);
276
277
  }
277
278
  return flattenWithBlankLine(lines, true);
@@ -1230,8 +1231,8 @@ export function markdownToBlocks(markdown) {
1230
1231
  type: "file",
1231
1232
  props: {
1232
1233
  name: fileMatch[1] || "",
1233
- caption: fileMatch[2] || "",
1234
1234
  url: fileMatch[3],
1235
+ caption: fileMatch[2] || "",
1235
1236
  },
1236
1237
  children: [],
1237
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
  },
@@ -0,0 +1,3 @@
1
+ export type FileDisplayUrlResolver = (fileUrl: string) => string;
2
+ export declare function setFileDisplayUrlResolver(fn: FileDisplayUrlResolver | null): void;
3
+ export declare function resolveFileDisplayUrl(fileUrl: string): string;
@@ -0,0 +1,10 @@
1
+ let resolver = null;
2
+ export function setFileDisplayUrlResolver(fn) {
3
+ resolver = fn;
4
+ }
5
+ export function resolveFileDisplayUrl(fileUrl) {
6
+ if (resolver) {
7
+ return resolver(fileUrl);
8
+ }
9
+ return `/images/file-type-icons/file.svg`;
10
+ }
@@ -5,5 +5,6 @@ export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
5
5
  export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, } from "./editor/customMarkdownConverter";
6
6
  export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
7
7
  export { useStepImageUpload, setImageUploadHandler, type StepImageUploadHandler, } from "./editor/stepImageUpload";
8
+ export { setFileDisplayUrlResolver, resolveFileDisplayUrl, type FileDisplayUrlResolver, } from "./editor/fileDisplayUrl";
8
9
  export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
9
10
  export declare const testomatioEditorClassName = "markdown testomatio-editor";
package/package/index.js CHANGED
@@ -5,5 +5,6 @@ export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
5
5
  export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
6
6
  export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
7
7
  export { useStepImageUpload, setImageUploadHandler, } from "./editor/stepImageUpload";
8
+ export { setFileDisplayUrlResolver, resolveFileDisplayUrl, } from "./editor/fileDisplayUrl";
8
9
  export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
9
10
  export const testomatioEditorClassName = "markdown testomatio-editor";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.30",
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
+ });
@@ -6,6 +6,7 @@ import {
6
6
  type CustomEditorBlock,
7
7
  type CustomPartialBlock,
8
8
  } from "./customMarkdownConverter";
9
+ import { setFileDisplayUrlResolver } from "./fileDisplayUrl";
9
10
 
10
11
  const baseProps = {
11
12
  textAlignment: "left" as const,
@@ -1515,7 +1516,7 @@ describe("markdownToBlocks", () => {
1515
1516
  });
1516
1517
 
1517
1518
  describe("file block serialization", () => {
1518
- it("serializes a file block with name, caption (display_url), and url", () => {
1519
+ it("serializes a file block using caption as display url", () => {
1519
1520
  const blocks: CustomEditorBlock[] = [
1520
1521
  {
1521
1522
  id: "1",
@@ -1534,7 +1535,7 @@ describe("file block serialization", () => {
1534
1535
  expect(md).toBe("[![report.pdf](/images/file-type-icons/pdf.svg)](https://example.com/file.pdf)");
1535
1536
  });
1536
1537
 
1537
- it("falls back to url when caption is empty", () => {
1538
+ it("derives icon from url when no caption is set", () => {
1538
1539
  const blocks: CustomEditorBlock[] = [
1539
1540
  {
1540
1541
  id: "1",
@@ -1542,7 +1543,30 @@ describe("file block serialization", () => {
1542
1543
  props: {
1543
1544
  ...baseProps,
1544
1545
  url: "https://example.com/file.pdf",
1545
- name: "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)");
1555
+ });
1556
+
1557
+ it("uses custom resolver when set", () => {
1558
+ setFileDisplayUrlResolver(() => {
1559
+ return `/custom-icons/file.svg`;
1560
+ });
1561
+
1562
+ const blocks: CustomEditorBlock[] = [
1563
+ {
1564
+ id: "1",
1565
+ type: "file",
1566
+ props: {
1567
+ ...baseProps,
1568
+ url: "https://example.com/file.pdf",
1569
+ name: "",
1546
1570
  caption: "",
1547
1571
  },
1548
1572
  content: undefined as any,
@@ -1550,7 +1574,9 @@ describe("file block serialization", () => {
1550
1574
  },
1551
1575
  ];
1552
1576
  const md = blocksToMarkdown(blocks);
1553
- expect(md).toBe("[![file.pdf](https://example.com/file.pdf)](https://example.com/file.pdf)");
1577
+ expect(md).toBe("[![](/custom-icons/file.svg)](https://example.com/file.pdf)");
1578
+
1579
+ setFileDisplayUrlResolver(null);
1554
1580
  });
1555
1581
 
1556
1582
  it("outputs nothing when url is empty", () => {
@@ -1574,23 +1600,24 @@ describe("file block serialization", () => {
1574
1600
  });
1575
1601
 
1576
1602
  describe("file block parsing", () => {
1577
- it("parses file markdown into a file block", () => {
1603
+ it("parses file markdown and stores display url in caption", () => {
1578
1604
  const markdown = "[![report.pdf](/images/file-type-icons/pdf.svg)](https://example.com/file.pdf)";
1579
1605
  const blocks = markdownToBlocks(markdown);
1580
1606
  expect(blocks).toHaveLength(1);
1581
1607
  expect(blocks[0].type).toBe("file");
1582
1608
  expect((blocks[0].props as any).name).toBe("report.pdf");
1583
- expect((blocks[0].props as any).caption).toBe("/images/file-type-icons/pdf.svg");
1584
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");
1585
1611
  });
1586
1612
 
1587
1613
  it("parses file markdown with empty name", () => {
1588
- const markdown = "[![](https://example.com/url)](https://example.com/url)";
1614
+ const markdown = "[![](/images/file-type-icons/json.svg)](https://example.com/file.json)";
1589
1615
  const blocks = markdownToBlocks(markdown);
1590
1616
  expect(blocks).toHaveLength(1);
1591
1617
  expect(blocks[0].type).toBe("file");
1592
1618
  expect((blocks[0].props as any).name).toBe("");
1593
- 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");
1594
1621
  });
1595
1622
 
1596
1623
  it("does not confuse file blocks with image blocks", () => {
@@ -1600,7 +1627,7 @@ describe("file block parsing", () => {
1600
1627
  expect(blocks[0].type).toBe("image");
1601
1628
  });
1602
1629
 
1603
- it("round-trips file blocks through serialize and parse", () => {
1630
+ it("round-trips file blocks preserving icon url in caption", () => {
1604
1631
  const blocks: CustomEditorBlock[] = [
1605
1632
  {
1606
1633
  id: "1",
@@ -1616,6 +1643,8 @@ describe("file block parsing", () => {
1616
1643
  },
1617
1644
  ];
1618
1645
  const md = blocksToMarkdown(blocks);
1646
+ expect(md).toBe("[![doc.xlsx](/images/file-type-icons/xlsx.svg)](https://example.com/doc.xlsx)");
1647
+
1619
1648
  const parsed = markdownToBlocks(md);
1620
1649
  expect(parsed).toHaveLength(1);
1621
1650
  expect(parsed[0].type).toBe("file");
@@ -1623,4 +1652,11 @@ describe("file block parsing", () => {
1623
1652
  expect((parsed[0].props as any).name).toBe("doc.xlsx");
1624
1653
  expect((parsed[0].props as any).caption).toBe("/images/file-type-icons/xlsx.svg");
1625
1654
  });
1655
+
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);
1661
+ });
1626
1662
  });
@@ -8,6 +8,7 @@ import type {
8
8
  PartialBlock,
9
9
  Styles,
10
10
  } from "@blocknote/core";
11
+ import { resolveFileDisplayUrl } from "./fileDisplayUrl";
11
12
  import type { customSchema } from "./customSchema";
12
13
  import { isStepsHeading } from "./blocks/step";
13
14
 
@@ -349,7 +350,7 @@ function serializeBlock(
349
350
  const name = (block.props as any).name || "";
350
351
  const caption = (block.props as any).caption || "";
351
352
  if (url) {
352
- const displayUrl = caption || url;
353
+ const displayUrl = caption || resolveFileDisplayUrl(url);
353
354
  lines.push(`[![${name}](${displayUrl})](${url})`);
354
355
  }
355
356
  return flattenWithBlankLine(lines, true);
@@ -1472,8 +1473,8 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1472
1473
  type: "file",
1473
1474
  props: {
1474
1475
  name: fileMatch[1] || "",
1475
- caption: fileMatch[2] || "",
1476
1476
  url: fileMatch[3],
1477
+ caption: fileMatch[2] || "",
1477
1478
  },
1478
1479
  children: [],
1479
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
  },
@@ -0,0 +1,14 @@
1
+ export type FileDisplayUrlResolver = (fileUrl: string) => string;
2
+
3
+ let resolver: FileDisplayUrlResolver | null = null;
4
+
5
+ export function setFileDisplayUrlResolver(fn: FileDisplayUrlResolver | null) {
6
+ resolver = fn;
7
+ }
8
+
9
+ export function resolveFileDisplayUrl(fileUrl: string): string {
10
+ if (resolver) {
11
+ return resolver(fileUrl);
12
+ }
13
+ return `/images/file-type-icons/file.svg`;
14
+ }
package/src/index.ts CHANGED
@@ -30,6 +30,12 @@ export {
30
30
  type StepImageUploadHandler,
31
31
  } from "./editor/stepImageUpload";
32
32
 
33
+ export {
34
+ setFileDisplayUrlResolver,
35
+ resolveFileDisplayUrl,
36
+ type FileDisplayUrlResolver,
37
+ } from "./editor/fileDisplayUrl";
38
+
33
39
  export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
34
40
 
35
41
  export const testomatioEditorClassName = "markdown testomatio-editor";