testomatio-editor-blocks 0.4.77 → 0.4.78
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.
- package/package/editor/MentionAutocomplete.d.ts +38 -0
- package/package/editor/MentionAutocomplete.js +312 -0
- package/package/editor/MentionMenu.d.ts +17 -0
- package/package/editor/MentionMenu.js +35 -0
- package/package/editor/mentionAutocomplete.d.ts +152 -0
- package/package/editor/mentionAutocomplete.js +218 -0
- package/package/index.d.ts +3 -0
- package/package/index.js +3 -0
- package/package/styles.css +75 -0
- package/package.json +1 -1
- package/src/App.tsx +77 -0
- package/src/editor/MentionAutocomplete.tsx +463 -0
- package/src/editor/MentionMenu.tsx +46 -0
- package/src/editor/mentionAutocomplete.test.ts +267 -0
- package/src/editor/mentionAutocomplete.ts +348 -0
- package/src/editor/styles.css +75 -0
- package/src/index.ts +31 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal, prefix-based `@`-mention autocomplete.
|
|
3
|
+
*
|
|
4
|
+
* A single mechanism that turns typed tokens like `@`, `@T`, `@S` into inserted
|
|
5
|
+
* references such as `@T123456`, `@S98765`, or `@username`. Which list is shown
|
|
6
|
+
* (and what gets inserted) is decided by a configurable set of {@link MentionSource}s,
|
|
7
|
+
* each keyed by the characters that follow the `@`:
|
|
8
|
+
*
|
|
9
|
+
* { prefix: "T", search: (q) => api.findTests(q) } // @T -> fetch tests -> "@T123456"
|
|
10
|
+
* { prefix: "S", search: (q) => api.findSuites(q) } // @S -> fetch suites -> "@S98765"
|
|
11
|
+
* { prefix: "", items: users, insert: (u) => `@${u.label}` } // @ -> filter users -> "@john"
|
|
12
|
+
*
|
|
13
|
+
* This file is intentionally DOM-free: it holds only the pure logic (matching,
|
|
14
|
+
* source resolution, filtering, token building) plus the global source registry.
|
|
15
|
+
* The React wiring lives in `MentionAutocomplete.tsx`.
|
|
16
|
+
*/
|
|
17
|
+
/* ------------------------------------------------------------------ *
|
|
18
|
+
* Global source registry (dependency-injection seam, mirrors the rest
|
|
19
|
+
* of the library's `setXFetcher` pattern).
|
|
20
|
+
* ------------------------------------------------------------------ */
|
|
21
|
+
let globalSources = [];
|
|
22
|
+
/** Register the mention sources used when a consumer doesn't pass `sources` explicitly. */
|
|
23
|
+
export function setMentionSources(sources) {
|
|
24
|
+
globalSources = Array.isArray(sources) ? sources : [];
|
|
25
|
+
}
|
|
26
|
+
/** Read the globally-registered mention sources. */
|
|
27
|
+
export function getMentionSources() {
|
|
28
|
+
return globalSources;
|
|
29
|
+
}
|
|
30
|
+
/* ------------------------------------------------------------------ *
|
|
31
|
+
* Pure logic
|
|
32
|
+
* ------------------------------------------------------------------ */
|
|
33
|
+
const WHITESPACE = /\s/;
|
|
34
|
+
/**
|
|
35
|
+
* Pick the source whose `prefix` matches the start of `token`. Longest prefix
|
|
36
|
+
* wins; the empty-prefix source is the ultimate fallback. Returns `null` when
|
|
37
|
+
* nothing matches (e.g. `@T` typed but only a `""` source is registered and it
|
|
38
|
+
* is absent).
|
|
39
|
+
*/
|
|
40
|
+
export function resolveMentionSource(token, sources) {
|
|
41
|
+
var _a, _b;
|
|
42
|
+
let best = null;
|
|
43
|
+
for (const source of sources) {
|
|
44
|
+
const prefix = (_a = source.prefix) !== null && _a !== void 0 ? _a : "";
|
|
45
|
+
if (!token.startsWith(prefix))
|
|
46
|
+
continue;
|
|
47
|
+
if (best === null || prefix.length > ((_b = best.prefix) !== null && _b !== void 0 ? _b : "").length) {
|
|
48
|
+
best = source;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return best;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Detect the mention being edited at `caret` within `text`.
|
|
55
|
+
*
|
|
56
|
+
* A mention is the run of non-whitespace characters after an `@` that sits at
|
|
57
|
+
* the start of the text or immediately after whitespace (so emails like
|
|
58
|
+
* `a@b.com` never trigger). Returns `null` when the caret is not inside such a
|
|
59
|
+
* token or no source matches.
|
|
60
|
+
*/
|
|
61
|
+
export function parseActiveMention(text, caret, sources) {
|
|
62
|
+
var _a;
|
|
63
|
+
if (!sources.length)
|
|
64
|
+
return null;
|
|
65
|
+
const pos = Math.max(0, Math.min(caret, text.length));
|
|
66
|
+
const before = text.slice(0, pos);
|
|
67
|
+
const at = before.lastIndexOf("@");
|
|
68
|
+
if (at === -1)
|
|
69
|
+
return null;
|
|
70
|
+
// The `@` must start a word: preceded by nothing or whitespace.
|
|
71
|
+
const charBefore = at === 0 ? "" : text[at - 1];
|
|
72
|
+
if (charBefore !== "" && !WHITESPACE.test(charBefore))
|
|
73
|
+
return null;
|
|
74
|
+
// The token is everything from just after `@` up to the caret. Whitespace
|
|
75
|
+
// ends a mention, so a caret past a space means we're no longer inside one.
|
|
76
|
+
const token = before.slice(at + 1);
|
|
77
|
+
if (WHITESPACE.test(token))
|
|
78
|
+
return null;
|
|
79
|
+
const source = resolveMentionSource(token, sources);
|
|
80
|
+
if (!source)
|
|
81
|
+
return null;
|
|
82
|
+
const prefix = (_a = source.prefix) !== null && _a !== void 0 ? _a : "";
|
|
83
|
+
return {
|
|
84
|
+
source,
|
|
85
|
+
prefix,
|
|
86
|
+
query: token.slice(prefix.length),
|
|
87
|
+
start: at,
|
|
88
|
+
end: pos,
|
|
89
|
+
token,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** The text a chosen item inserts, honoring item/source overrides. */
|
|
93
|
+
export function buildMentionInsertText(source, item) {
|
|
94
|
+
var _a;
|
|
95
|
+
if (typeof item.insertText === "string")
|
|
96
|
+
return item.insertText;
|
|
97
|
+
if (typeof source.insert === "function")
|
|
98
|
+
return source.insert(item, source);
|
|
99
|
+
return `@${(_a = source.prefix) !== null && _a !== void 0 ? _a : ""}${item.id}`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Replace the active mention token in `text` with `insertText`, adding a single
|
|
103
|
+
* trailing space so the caret lands ready for the next word and the freshly
|
|
104
|
+
* inserted `@…` token does not immediately re-trigger the popup.
|
|
105
|
+
*/
|
|
106
|
+
export function applyMention(text, active, insertText) {
|
|
107
|
+
const before = text.slice(0, active.start);
|
|
108
|
+
const after = text.slice(active.end);
|
|
109
|
+
const hasLeadingSpace = after.length > 0 && WHITESPACE.test(after[0]);
|
|
110
|
+
const separator = hasLeadingSpace ? "" : " ";
|
|
111
|
+
return {
|
|
112
|
+
text: `${before}${insertText}${separator}${after}`,
|
|
113
|
+
// Land just past the single following space (added or pre-existing) so the
|
|
114
|
+
// caret is ready for the next word.
|
|
115
|
+
caret: before.length + insertText.length + 1,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Rank static items against a query: label starts-with beats label contains,
|
|
120
|
+
* which beats id matches. Case-insensitive. An empty query returns the head of
|
|
121
|
+
* the list unchanged.
|
|
122
|
+
*/
|
|
123
|
+
export function filterMentionItems(items, query, limit = 8) {
|
|
124
|
+
const q = query.trim().toLowerCase();
|
|
125
|
+
if (!q)
|
|
126
|
+
return items.slice(0, limit);
|
|
127
|
+
const scored = [];
|
|
128
|
+
items.forEach((item, order) => {
|
|
129
|
+
var _a, _b, _c;
|
|
130
|
+
const label = ((_a = item.label) !== null && _a !== void 0 ? _a : "").toLowerCase();
|
|
131
|
+
const id = String((_b = item.id) !== null && _b !== void 0 ? _b : "").toLowerCase();
|
|
132
|
+
const detail = ((_c = item.detail) !== null && _c !== void 0 ? _c : "").toString().toLowerCase();
|
|
133
|
+
let score = -1;
|
|
134
|
+
if (label.startsWith(q))
|
|
135
|
+
score = 0;
|
|
136
|
+
else if (label.includes(q))
|
|
137
|
+
score = 1;
|
|
138
|
+
else if (id.startsWith(q) || id.includes(q))
|
|
139
|
+
score = 2;
|
|
140
|
+
else if (detail.includes(q))
|
|
141
|
+
score = 3;
|
|
142
|
+
if (score >= 0)
|
|
143
|
+
scored.push({ item, score, order });
|
|
144
|
+
});
|
|
145
|
+
scored.sort((a, b) => (a.score - b.score) || (a.order - b.order));
|
|
146
|
+
return scored.slice(0, limit).map((s) => s.item);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Route a typed query (everything after the trigger char, e.g. `T123` or `bob`)
|
|
150
|
+
* to its source and resolve the items to show. Handles prefix stripping,
|
|
151
|
+
* `minChars`, async `search`, static `items` (incl. provider functions),
|
|
152
|
+
* normalization, and the `limit`. Returns `null` when no source matches.
|
|
153
|
+
*
|
|
154
|
+
* Shared by both mention frontends (the textarea hook and the BlockNote menu).
|
|
155
|
+
*/
|
|
156
|
+
export async function resolveMentionQuery(query, sources) {
|
|
157
|
+
var _a, _b, _c;
|
|
158
|
+
const source = resolveMentionSource(query, sources);
|
|
159
|
+
if (!source)
|
|
160
|
+
return null;
|
|
161
|
+
const prefix = (_a = source.prefix) !== null && _a !== void 0 ? _a : "";
|
|
162
|
+
const sub = query.slice(prefix.length);
|
|
163
|
+
const limit = (_b = source.limit) !== null && _b !== void 0 ? _b : 8;
|
|
164
|
+
if (typeof source.search === "function") {
|
|
165
|
+
const minChars = (_c = source.minChars) !== null && _c !== void 0 ? _c : 0;
|
|
166
|
+
if (sub.length < minChars)
|
|
167
|
+
return { source, items: [] };
|
|
168
|
+
const items = normalizeMentionItems(await source.search(sub)).slice(0, limit);
|
|
169
|
+
return { source, items };
|
|
170
|
+
}
|
|
171
|
+
const input = source.items;
|
|
172
|
+
const raw = typeof input === "function" ? await input() : input !== null && input !== void 0 ? input : [];
|
|
173
|
+
const items = filterMentionItems(normalizeMentionItems(raw), sub, limit);
|
|
174
|
+
return { source, items };
|
|
175
|
+
}
|
|
176
|
+
/* ------------------------------------------------------------------ *
|
|
177
|
+
* Normalization of async / JSON:API results into MentionItem[]
|
|
178
|
+
* ------------------------------------------------------------------ */
|
|
179
|
+
export function parseMentionsFromJsonApi(document) {
|
|
180
|
+
const resources = Array.isArray(document) ? document : document === null || document === void 0 ? void 0 : document.data;
|
|
181
|
+
if (!Array.isArray(resources) || resources.length === 0)
|
|
182
|
+
return [];
|
|
183
|
+
return resources
|
|
184
|
+
.map((resource) => normalizeJsonApiResource(resource))
|
|
185
|
+
.filter((value) => Boolean(value));
|
|
186
|
+
}
|
|
187
|
+
/** Coerce any supported `search`/`items` result into a clean `MentionItem[]`. */
|
|
188
|
+
export function normalizeMentionItems(result) {
|
|
189
|
+
if (!result)
|
|
190
|
+
return [];
|
|
191
|
+
if (Array.isArray(result)) {
|
|
192
|
+
if (result.length === 0)
|
|
193
|
+
return [];
|
|
194
|
+
if (isMentionItemArray(result))
|
|
195
|
+
return result;
|
|
196
|
+
return parseMentionsFromJsonApi(result);
|
|
197
|
+
}
|
|
198
|
+
return parseMentionsFromJsonApi(result);
|
|
199
|
+
}
|
|
200
|
+
function normalizeJsonApiResource(resource) {
|
|
201
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
202
|
+
if (!resource)
|
|
203
|
+
return null;
|
|
204
|
+
const attrs = (_a = resource.attributes) !== null && _a !== void 0 ? _a : {};
|
|
205
|
+
const id = resource.id;
|
|
206
|
+
if (id === null || id === undefined || id === "")
|
|
207
|
+
return null;
|
|
208
|
+
const label = (_e = (_d = (_c = (_b = attrs.title) !== null && _b !== void 0 ? _b : attrs.name) !== null && _c !== void 0 ? _c : attrs.label) !== null && _d !== void 0 ? _d : attrs.username) !== null && _e !== void 0 ? _e : String(id);
|
|
209
|
+
return {
|
|
210
|
+
id: String(id),
|
|
211
|
+
label: String(label),
|
|
212
|
+
detail: (_h = (_g = (_f = attrs.detail) !== null && _f !== void 0 ? _f : attrs.description) !== null && _g !== void 0 ? _g : attrs.email) !== null && _h !== void 0 ? _h : null,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function isMentionItemArray(value) {
|
|
216
|
+
const first = value[0];
|
|
217
|
+
return Boolean(first) && typeof (first === null || first === void 0 ? void 0 : first.label) === "string";
|
|
218
|
+
}
|
package/package/index.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export { tagBadgeExtension, TagBadgeExtension, TAGS_DETECT_REGEXP, detectTags, t
|
|
|
8
8
|
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, type MarkdownToBlocksOptions, } from "./editor/customMarkdownConverter";
|
|
9
9
|
export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
|
|
10
10
|
export { useStepImageUpload, setImageUploadHandler, type StepImageUploadHandler, } from "./editor/stepImageUpload";
|
|
11
|
+
export { setMentionSources, getMentionSources, parseActiveMention, resolveMentionSource, buildMentionInsertText, applyMention, filterMentionItems, resolveMentionQuery, normalizeMentionItems, parseMentionsFromJsonApi, type MentionSource, type MentionItem, type MentionItemsInput, type MentionSearchResult, type ActiveMention, type MentionJsonApiDocument, type MentionJsonApiResource, } from "./editor/mentionAutocomplete";
|
|
12
|
+
export { MentionMenu, type MentionMenuProps, } from "./editor/MentionMenu";
|
|
13
|
+
export { useMentionAutocomplete, type UseMentionAutocompleteOptions, type UseMentionAutocompleteResult, } from "./editor/MentionAutocomplete";
|
|
11
14
|
export { setFileDisplayUrlResolver, resolveFileDisplayUrl, type FileDisplayUrlResolver, } from "./editor/fileDisplayUrl";
|
|
12
15
|
export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
|
|
13
16
|
export declare const testomatioEditorClassName = "markdown testomatio-editor";
|
package/package/index.js
CHANGED
|
@@ -8,6 +8,9 @@ export { tagBadgeExtension, TagBadgeExtension, TAGS_DETECT_REGEXP, detectTags, }
|
|
|
8
8
|
export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
|
|
9
9
|
export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
|
|
10
10
|
export { useStepImageUpload, setImageUploadHandler, } from "./editor/stepImageUpload";
|
|
11
|
+
export { setMentionSources, getMentionSources, parseActiveMention, resolveMentionSource, buildMentionInsertText, applyMention, filterMentionItems, resolveMentionQuery, normalizeMentionItems, parseMentionsFromJsonApi, } from "./editor/mentionAutocomplete";
|
|
12
|
+
export { MentionMenu, } from "./editor/MentionMenu";
|
|
13
|
+
export { useMentionAutocomplete, } from "./editor/MentionAutocomplete";
|
|
11
14
|
export { setFileDisplayUrlResolver, resolveFileDisplayUrl, } from "./editor/fileDisplayUrl";
|
|
12
15
|
export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
|
|
13
16
|
export const testomatioEditorClassName = "markdown testomatio-editor";
|
package/package/styles.css
CHANGED
|
@@ -1844,6 +1844,81 @@ html.dark .bn-step-editor--preview a.step-preview-link {
|
|
|
1844
1844
|
color: var(--step-muted);
|
|
1845
1845
|
}
|
|
1846
1846
|
|
|
1847
|
+
/* Universal @-mention autocomplete popup (portaled to <body>, fixed to caret). */
|
|
1848
|
+
.bn-mention-popup {
|
|
1849
|
+
position: fixed;
|
|
1850
|
+
min-width: 15rem;
|
|
1851
|
+
max-width: 24rem;
|
|
1852
|
+
background: var(--bg-white-opaque);
|
|
1853
|
+
border-radius: 0.65rem;
|
|
1854
|
+
border: 1px solid var(--step-border-light);
|
|
1855
|
+
box-shadow: 0 18px 35px var(--shadow-medium);
|
|
1856
|
+
padding: 0.25rem;
|
|
1857
|
+
display: flex;
|
|
1858
|
+
flex-direction: column;
|
|
1859
|
+
gap: 0.1rem;
|
|
1860
|
+
z-index: 9999;
|
|
1861
|
+
max-height: 15rem;
|
|
1862
|
+
overflow-y: auto;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
.bn-mention-popup__header {
|
|
1866
|
+
display: flex;
|
|
1867
|
+
align-items: center;
|
|
1868
|
+
gap: 0.4rem;
|
|
1869
|
+
padding: 0.3rem 0.6rem 0.35rem;
|
|
1870
|
+
font-size: 0.72rem;
|
|
1871
|
+
text-transform: uppercase;
|
|
1872
|
+
letter-spacing: 0.04em;
|
|
1873
|
+
color: var(--step-muted);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
.bn-mention-popup__prefix {
|
|
1877
|
+
font-weight: 700;
|
|
1878
|
+
color: var(--text-primary);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
.bn-mention-popup__status {
|
|
1882
|
+
padding: 0.5rem 0.75rem;
|
|
1883
|
+
font-size: 0.85rem;
|
|
1884
|
+
color: var(--step-muted);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
.bn-mention-item {
|
|
1888
|
+
border: none;
|
|
1889
|
+
background: transparent;
|
|
1890
|
+
border-radius: 0.5rem;
|
|
1891
|
+
padding: 0.4rem 0.75rem;
|
|
1892
|
+
text-align: left;
|
|
1893
|
+
display: flex;
|
|
1894
|
+
align-items: baseline;
|
|
1895
|
+
justify-content: space-between;
|
|
1896
|
+
gap: 0.75rem;
|
|
1897
|
+
cursor: pointer;
|
|
1898
|
+
color: var(--text-primary);
|
|
1899
|
+
width: 100%;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
.bn-mention-item:hover,
|
|
1903
|
+
.bn-mention-item--active {
|
|
1904
|
+
background: var(--step-bg-light);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
.bn-mention-item__label {
|
|
1908
|
+
font-size: 0.92rem;
|
|
1909
|
+
white-space: nowrap;
|
|
1910
|
+
overflow: hidden;
|
|
1911
|
+
text-overflow: ellipsis;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
.bn-mention-item__detail {
|
|
1915
|
+
font-size: 0.75rem;
|
|
1916
|
+
font-weight: 500;
|
|
1917
|
+
color: var(--step-muted);
|
|
1918
|
+
white-space: nowrap;
|
|
1919
|
+
flex-shrink: 0;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1847
1922
|
.bn-inline-image {
|
|
1848
1923
|
display: block;
|
|
1849
1924
|
max-width: 100%;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -23,6 +23,13 @@ import { customSchema, type CustomEditor } from "./editor/customSchema";
|
|
|
23
23
|
import { tagBadgeExtension } from "./editor/tagBadge";
|
|
24
24
|
import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
|
|
25
25
|
import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
|
|
26
|
+
import {
|
|
27
|
+
setMentionSources,
|
|
28
|
+
type MentionItem,
|
|
29
|
+
type MentionSearchResult,
|
|
30
|
+
type MentionSource,
|
|
31
|
+
} from "./editor/mentionAutocomplete";
|
|
32
|
+
import { MentionMenu } from "./editor/MentionMenu";
|
|
26
33
|
import { setImageUploadHandler } from "./editor/stepImageUpload";
|
|
27
34
|
import { canInsertStepOrSnippet, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
28
35
|
import { addTestBlock } from "./editor/blocks/testMeta";
|
|
@@ -287,6 +294,73 @@ const DEMO_SNIPPET_FIXTURES: SnippetJsonApiDocument = {
|
|
|
287
294
|
],
|
|
288
295
|
};
|
|
289
296
|
|
|
297
|
+
const DEMO_USERS: MentionItem[] = [
|
|
298
|
+
{ id: "u-davert", label: "davert", detail: "Michael Bodnarchuk" },
|
|
299
|
+
{ id: "u-anna", label: "anna", detail: "Anna QA" },
|
|
300
|
+
{ id: "u-taras", label: "taras", detail: "Taras Manager" },
|
|
301
|
+
{ id: "u-siren", label: "siern", detail: "Sirena QA" },
|
|
302
|
+
{ id: "u-bob", label: "bob", detail: "Bob Tester" },
|
|
303
|
+
{ id: "u-carol", label: "carol", detail: "Carol Dev" },
|
|
304
|
+
{ id: "u-dan", label: "dan", detail: "Dan Ops" },
|
|
305
|
+
{ id: "u-eve", label: "eve", detail: "Eve Security" },
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
// Simulated "tests" API dataset — insertion uses the numeric id -> "@T{id}".
|
|
309
|
+
const DEMO_TESTS: MentionItem[] = [
|
|
310
|
+
{ id: "10231", label: "Login redirects to dashboard", detail: "#10231" },
|
|
311
|
+
{ id: "10232", label: "Login rejects wrong password", detail: "#10232" },
|
|
312
|
+
{ id: "10245", label: "Logout clears the session", detail: "#10245" },
|
|
313
|
+
{ id: "10310", label: "Checkout applies discount code", detail: "#10310" },
|
|
314
|
+
{ id: "10311", label: "Checkout blocks empty cart", detail: "#10311" },
|
|
315
|
+
{ id: "10420", label: "Profile avatar upload", detail: "#10420" },
|
|
316
|
+
{ id: "10421", label: "Profile email change requires confirm", detail: "#10421" },
|
|
317
|
+
{ id: "10530", label: "Password reset sends email", detail: "#10530" },
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const DEMO_SUITES: MentionItem[] = [
|
|
321
|
+
{ id: "1001", label: "Authentication", detail: "#1001" },
|
|
322
|
+
{ id: "1002", label: "Checkout flow", detail: "#1002" },
|
|
323
|
+
{ id: "1003", label: "User profile", detail: "#1003" },
|
|
324
|
+
{ id: "1004", label: "Notifications", detail: "#1004" },
|
|
325
|
+
{ id: "1005", label: "Admin panel", detail: "#1005" },
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
const demoDelay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Demo @-mention config. Prefixes are fully data-driven: @T / @S hit a
|
|
332
|
+
* (simulated) API and insert an id token, while a bare @ filters the in-memory
|
|
333
|
+
* user list and inserts the handle.
|
|
334
|
+
*/
|
|
335
|
+
const DEMO_MENTION_SOURCES: MentionSource[] = [
|
|
336
|
+
{
|
|
337
|
+
prefix: "T",
|
|
338
|
+
label: "Tests",
|
|
339
|
+
search: async (query): Promise<MentionSearchResult> => {
|
|
340
|
+
await demoDelay(120); // simulate an API round-trip
|
|
341
|
+
const q = query.toLowerCase();
|
|
342
|
+
return DEMO_TESTS.filter((test) => test.label.toLowerCase().includes(q));
|
|
343
|
+
},
|
|
344
|
+
// default insert -> "@T{id}", e.g. "@T10231"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
prefix: "S",
|
|
348
|
+
label: "Suites",
|
|
349
|
+
search: async (query): Promise<MentionSearchResult> => {
|
|
350
|
+
await demoDelay(120);
|
|
351
|
+
const q = query.toLowerCase();
|
|
352
|
+
return DEMO_SUITES.filter((suite) => suite.label.toLowerCase().includes(q));
|
|
353
|
+
},
|
|
354
|
+
// default insert -> "@S{id}", e.g. "@S1001"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
prefix: "",
|
|
358
|
+
label: "Users",
|
|
359
|
+
items: DEMO_USERS,
|
|
360
|
+
insert: (item) => `@${item.label}`, // insert -> "@davert"
|
|
361
|
+
},
|
|
362
|
+
];
|
|
363
|
+
|
|
290
364
|
function CustomSlashMenu() {
|
|
291
365
|
const editor = useBlockNoteEditor<Schema["blockSchema"], Schema["inlineContentSchema"], Schema["styleSchema"]>();
|
|
292
366
|
|
|
@@ -512,6 +586,7 @@ function App() {
|
|
|
512
586
|
// Demo defaults: configure global handlers so the editor works without manual providers.
|
|
513
587
|
setStepsFetcher(() => DEMO_STEP_FIXTURES);
|
|
514
588
|
setSnippetFetcher(() => DEMO_SNIPPET_FIXTURES);
|
|
589
|
+
setMentionSources(DEMO_MENTION_SOURCES);
|
|
515
590
|
|
|
516
591
|
const handler = editor?.uploadFile
|
|
517
592
|
? async (file: Blob) => {
|
|
@@ -538,6 +613,7 @@ function App() {
|
|
|
538
613
|
setStepsFetcher(null);
|
|
539
614
|
setSnippetFetcher(null);
|
|
540
615
|
setImageUploadHandler(null);
|
|
616
|
+
setMentionSources(null);
|
|
541
617
|
};
|
|
542
618
|
}, [editor, uploadStepImage]);
|
|
543
619
|
|
|
@@ -675,6 +751,7 @@ function App() {
|
|
|
675
751
|
className="markdown testomatio-editor"
|
|
676
752
|
>
|
|
677
753
|
<CustomSlashMenu />
|
|
754
|
+
<MentionMenu />
|
|
678
755
|
</BlockNoteView>
|
|
679
756
|
</div>
|
|
680
757
|
<aside className="app__preview">
|