testomatio-editor-blocks 0.2.0 → 0.2.2

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.
@@ -25,6 +25,7 @@ export const snippetBlock = createReactBlockSpec({
25
25
  const snippetTitle = block.props.snippetTitle || "";
26
26
  const snippetData = block.props.snippetData || "";
27
27
  const snippetSuggestions = useSnippetAutocomplete();
28
+ const hasSnippets = snippetSuggestions.length > 0;
28
29
  const handleSnippetChange = useCallback((nextTitle) => {
29
30
  if (nextTitle === snippetTitle) {
30
31
  return;
@@ -60,6 +61,12 @@ export const snippetBlock = createReactBlockSpec({
60
61
  },
61
62
  });
62
63
  }, [block.id, editor]);
63
- return (_jsxs("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: [_jsx(StepField, { label: "Snippet Title", value: snippetTitle, placeholder: "Describe the reusable action", onChange: handleSnippetChange, autoFocus: snippetTitle.length === 0, enableAutocomplete: true, suggestionFilter: (suggestion) => suggestion.isSnippet === true, suggestionsOverride: snippetSuggestions, onSuggestionSelect: handleSnippetSelect, fieldName: "snippet-title", showSuggestionsOnFocus: true, enableImageUpload: false }), _jsx(StepField, { label: "Snippet Data", value: snippetData, placeholder: "Add optional data or assets for the snippet", onChange: handleSnippetDataChange, multiline: true, fieldName: "snippet-data", enableImageUpload: true })] }));
64
+ const handleFieldFocus = useCallback(() => {
65
+ editor.setSelection(block.id, block.id);
66
+ }, [editor, block.id]);
67
+ if (!hasSnippets) {
68
+ return (_jsx("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: _jsx("p", { className: "bn-snippet__empty", children: "No snippets in this project." }) }));
69
+ }
70
+ return (_jsxs("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: [_jsx(StepField, { label: "Snippet Title", value: snippetTitle, placeholder: "Describe the reusable action", onChange: handleSnippetChange, autoFocus: snippetTitle.length === 0, enableAutocomplete: true, suggestionFilter: (suggestion) => suggestion.isSnippet === true, suggestionsOverride: snippetSuggestions, onSuggestionSelect: handleSnippetSelect, fieldName: "snippet-title", showSuggestionsOnFocus: true, enableImageUpload: false, onFieldFocus: handleFieldFocus }), _jsx(StepField, { label: "Snippet Data", value: snippetData, placeholder: "Add optional data or assets for the snippet", onChange: handleSnippetDataChange, multiline: true, fieldName: "snippet-data", enableImageUpload: true, onFieldFocus: handleFieldFocus })] }));
64
71
  },
65
72
  });
@@ -72,7 +72,23 @@ export const stepBlock = createReactBlockSpec({
72
72
  },
73
73
  });
74
74
  }, [editor, block.id, expectedResult]);
75
- return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, children: [_jsx(StepField, { label: "Step Title", value: stepTitle, placeholder: "Describe the action to perform", onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, rightAction: !isDataVisible ? (_jsx("button", { type: "button", className: "bn-teststep__toggle", onClick: handleShowDataField, "aria-expanded": "false", tabIndex: -1, children: "+ Step Data" })) : null, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
75
+ const handleInsertNextStep = useCallback(() => {
76
+ editor.insertBlocks([
77
+ {
78
+ type: "testStep",
79
+ props: {
80
+ stepTitle: "",
81
+ stepData: "",
82
+ expectedResult: "",
83
+ },
84
+ children: [],
85
+ },
86
+ ], block.id, "after");
87
+ }, [editor, block.id]);
88
+ const handleFieldFocus = useCallback(() => {
89
+ editor.setSelection(block.id, block.id);
90
+ }, [editor, block.id]);
91
+ return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, children: [_jsx(StepField, { label: "Step Title", value: stepTitle, placeholder: "Describe the action to perform", onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, rightAction: !isDataVisible ? (_jsx("button", { type: "button", className: "bn-teststep__toggle", onClick: handleShowDataField, "aria-expanded": "false", tabIndex: -1, children: "+ Step Data" })) : null, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
76
92
  if (!uploadImage) {
77
93
  return;
78
94
  }
@@ -92,6 +108,6 @@ export const stepBlock = createReactBlockSpec({
92
108
  catch (error) {
93
109
  console.error("Failed to upload image to Step Data", error);
94
110
  }
95
- } }), isDataVisible && (_jsx(StepField, { label: "Step Data", value: stepData, placeholder: "Provide additional data about the step", onChange: handleStepDataChange, autoFocus: shouldFocusDataField, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true })), showExpectedField && (_jsx(StepField, { label: "Expected Result", value: expectedResult, placeholder: "What should happen?", onChange: handleExpectedChange, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true }))] }));
111
+ } }), isDataVisible && (_jsx(StepField, { label: "Step Data", value: stepData, placeholder: "Provide additional data about the step", onChange: handleStepDataChange, autoFocus: shouldFocusDataField, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })), showExpectedField && (_jsx(StepField, { label: "Expected Result", value: expectedResult, placeholder: "What should happen?", onChange: handleExpectedChange, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })), _jsx("button", { type: "button", className: "bn-step-add", onClick: handleInsertNextStep, children: "+ Step" })] }));
96
112
  },
97
113
  });
@@ -21,6 +21,7 @@ type StepFieldProps = {
21
21
  rightAction?: ReactNode;
22
22
  showFormattingButtons?: boolean;
23
23
  showImageButton?: boolean;
24
+ onFieldFocus?: () => void;
24
25
  };
25
- export declare function StepField({ label, value, placeholder, onChange, autoFocus, multiline, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare function StepField({ label, value, placeholder, onChange, autoFocus, multiline, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, onFieldFocus, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
26
27
  export {};
@@ -3,7 +3,7 @@ import { useStepAutocomplete } from "../stepAutocomplete";
3
3
  import { useStepImageUpload } from "../stepImageUpload";
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { escapeHtml, escapeMarkdownText, htmlToMarkdown, markdownToHtml, normalizePlainText, } from "./markdown";
6
- export function StepField({ label, value, placeholder, onChange, autoFocus, multiline = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, }) {
6
+ export function StepField({ label, value, placeholder, onChange, autoFocus, multiline = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, onFieldFocus, }) {
7
7
  const editorRef = useRef(null);
8
8
  const [isFocused, setIsFocused] = useState(false);
9
9
  const autoFocusRef = useRef(false);
@@ -240,6 +240,7 @@ export function StepField({ label, value, placeholder, onChange, autoFocus, mult
240
240
  if (showSuggestionsOnFocus && enableAutocomplete) {
241
241
  setShowAllSuggestions(true);
242
242
  }
243
+ onFieldFocus === null || onFieldFocus === void 0 ? void 0 : onFieldFocus();
243
244
  setPlainTextValue((_b = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.innerText) !== null && _b !== void 0 ? _b : "");
244
245
  }, onBlur: () => {
245
246
  setIsFocused(false);
@@ -1,4 +1,7 @@
1
1
  export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, } from "./editor/customSchema";
2
+ export { stepBlock } from "./editor/blocks/step";
3
+ export { snippetBlock } from "./editor/blocks/snippet";
4
+ export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
2
5
  export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, } from "./editor/customMarkdownConverter";
3
6
  export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
4
7
  export { useStepImageUpload, setImageUploadHandler, type StepImageUploadHandler, } from "./editor/stepImageUpload";
package/package/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export { customSchema, } from "./editor/customSchema";
2
+ export { stepBlock } from "./editor/blocks/step";
3
+ export { snippetBlock } from "./editor/blocks/snippet";
4
+ export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
2
5
  export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
3
6
  export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
4
7
  export { useStepImageUpload, setImageUploadHandler, } from "./editor/stepImageUpload";
@@ -169,6 +169,33 @@
169
169
  color: rgba(15, 118, 110, 0.65);
170
170
  }
171
171
 
172
+ .bn-snippet__empty {
173
+ margin: 0;
174
+ font-size: 0.9rem;
175
+ color: rgba(15, 23, 42, 0.65);
176
+ }
177
+
178
+ .bn-step-add {
179
+ align-self: flex-start;
180
+ margin-top: 0.25rem;
181
+ border: none;
182
+ background: rgba(37, 99, 235, 0.12);
183
+ color: #1d4ed8;
184
+ font-weight: 600;
185
+ font-size: 0.85rem;
186
+ padding: 0.35rem 0.65rem;
187
+ border-radius: 0.5rem;
188
+ cursor: pointer;
189
+ transition:
190
+ background-color 120ms ease,
191
+ box-shadow 120ms ease;
192
+ }
193
+
194
+ .bn-step-add:hover {
195
+ background: rgba(37, 99, 235, 0.2);
196
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18);
197
+ }
198
+
172
199
  .bn-teststep__toggle {
173
200
  align-self: flex-start;
174
201
  padding: 0.35rem 0.6rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
@@ -28,6 +28,7 @@ export const snippetBlock = createReactBlockSpec(
28
28
  const snippetTitle = (block.props.snippetTitle as string) || "";
29
29
  const snippetData = (block.props.snippetData as string) || "";
30
30
  const snippetSuggestions = useSnippetAutocomplete();
31
+ const hasSnippets = snippetSuggestions.length > 0;
31
32
 
32
33
  const handleSnippetChange = useCallback(
33
34
  (nextTitle: string) => {
@@ -77,6 +78,18 @@ export const snippetBlock = createReactBlockSpec(
77
78
  [block.id, editor],
78
79
  );
79
80
 
81
+ const handleFieldFocus = useCallback(() => {
82
+ editor.setSelection(block.id, block.id);
83
+ }, [editor, block.id]);
84
+
85
+ if (!hasSnippets) {
86
+ return (
87
+ <div className="bn-teststep bn-snippet" data-block-id={block.id}>
88
+ <p className="bn-snippet__empty">No snippets in this project.</p>
89
+ </div>
90
+ );
91
+ }
92
+
80
93
  return (
81
94
  <div className="bn-teststep bn-snippet" data-block-id={block.id}>
82
95
  <StepField
@@ -92,6 +105,7 @@ export const snippetBlock = createReactBlockSpec(
92
105
  fieldName="snippet-title"
93
106
  showSuggestionsOnFocus
94
107
  enableImageUpload={false}
108
+ onFieldFocus={handleFieldFocus}
95
109
  />
96
110
  <StepField
97
111
  label="Snippet Data"
@@ -101,6 +115,7 @@ export const snippetBlock = createReactBlockSpec(
101
115
  multiline
102
116
  fieldName="snippet-data"
103
117
  enableImageUpload
118
+ onFieldFocus={handleFieldFocus}
104
119
  />
105
120
  </div>
106
121
  );
@@ -95,6 +95,28 @@ export const stepBlock = createReactBlockSpec(
95
95
  [editor, block.id, expectedResult],
96
96
  );
97
97
 
98
+ const handleInsertNextStep = useCallback(() => {
99
+ editor.insertBlocks(
100
+ [
101
+ {
102
+ type: "testStep",
103
+ props: {
104
+ stepTitle: "",
105
+ stepData: "",
106
+ expectedResult: "",
107
+ },
108
+ children: [],
109
+ },
110
+ ],
111
+ block.id,
112
+ "after",
113
+ );
114
+ }, [editor, block.id]);
115
+
116
+ const handleFieldFocus = useCallback(() => {
117
+ editor.setSelection(block.id, block.id);
118
+ }, [editor, block.id]);
119
+
98
120
  return (
99
121
  <div className="bn-teststep" data-block-id={block.id}>
100
122
  <StepField
@@ -106,6 +128,7 @@ export const stepBlock = createReactBlockSpec(
106
128
  enableAutocomplete
107
129
  fieldName="title"
108
130
  suggestionFilter={(suggestion) => (suggestion as StepSuggestion).isSnippet !== true}
131
+ onFieldFocus={handleFieldFocus}
109
132
  rightAction={
110
133
  !isDataVisible ? (
111
134
  <button
@@ -154,6 +177,7 @@ export const stepBlock = createReactBlockSpec(
154
177
  enableImageUpload
155
178
  showFormattingButtons
156
179
  showImageButton
180
+ onFieldFocus={handleFieldFocus}
157
181
  />
158
182
  )}
159
183
  {showExpectedField && (
@@ -166,8 +190,12 @@ export const stepBlock = createReactBlockSpec(
166
190
  enableImageUpload
167
191
  showFormattingButtons
168
192
  showImageButton
193
+ onFieldFocus={handleFieldFocus}
169
194
  />
170
195
  )}
196
+ <button type="button" className="bn-step-add" onClick={handleInsertNextStep}>
197
+ + Step
198
+ </button>
171
199
  </div>
172
200
  );
173
201
  },
@@ -32,6 +32,7 @@ type StepFieldProps = {
32
32
  rightAction?: ReactNode;
33
33
  showFormattingButtons?: boolean;
34
34
  showImageButton?: boolean;
35
+ onFieldFocus?: () => void;
35
36
  };
36
37
 
37
38
  export function StepField({
@@ -53,6 +54,7 @@ export function StepField({
53
54
  rightAction,
54
55
  showFormattingButtons = false,
55
56
  showImageButton = false,
57
+ onFieldFocus,
56
58
  }: StepFieldProps) {
57
59
  const editorRef = useRef<HTMLDivElement>(null);
58
60
  const [isFocused, setIsFocused] = useState(false);
@@ -376,6 +378,7 @@ export function StepField({
376
378
  if (showSuggestionsOnFocus && enableAutocomplete) {
377
379
  setShowAllSuggestions(true);
378
380
  }
381
+ onFieldFocus?.();
379
382
  setPlainTextValue(editorRef.current?.innerText ?? "");
380
383
  }}
381
384
  onBlur={() => {
@@ -169,6 +169,33 @@
169
169
  color: rgba(15, 118, 110, 0.65);
170
170
  }
171
171
 
172
+ .bn-snippet__empty {
173
+ margin: 0;
174
+ font-size: 0.9rem;
175
+ color: rgba(15, 23, 42, 0.65);
176
+ }
177
+
178
+ .bn-step-add {
179
+ align-self: flex-start;
180
+ margin-top: 0.25rem;
181
+ border: none;
182
+ background: rgba(37, 99, 235, 0.12);
183
+ color: #1d4ed8;
184
+ font-weight: 600;
185
+ font-size: 0.85rem;
186
+ padding: 0.35rem 0.65rem;
187
+ border-radius: 0.5rem;
188
+ cursor: pointer;
189
+ transition:
190
+ background-color 120ms ease,
191
+ box-shadow 120ms ease;
192
+ }
193
+
194
+ .bn-step-add:hover {
195
+ background: rgba(37, 99, 235, 0.2);
196
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18);
197
+ }
198
+
172
199
  .bn-teststep__toggle {
173
200
  align-self: flex-start;
174
201
  padding: 0.35rem 0.6rem;
package/src/index.ts CHANGED
@@ -4,6 +4,9 @@ export {
4
4
  type CustomBlock,
5
5
  type CustomEditor,
6
6
  } from "./editor/customSchema";
7
+ export { stepBlock } from "./editor/blocks/step";
8
+ export { snippetBlock } from "./editor/blocks/snippet";
9
+ export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
7
10
 
8
11
  export {
9
12
  blocksToMarkdown,