@qoretechnologies/reqraft 0.10.62-pr.142.g62c577f → 0.10.62

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.
Files changed (24) hide show
  1. package/dist/components/form/engine/CompactRow.js +1 -1
  2. package/dist/components/form/engine/CompactRow.js.map +1 -1
  3. package/dist/components/form/fields/rich-text/RichText.d.ts +22 -1
  4. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  5. package/dist/components/form/fields/rich-text/RichText.js +135 -50
  6. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  7. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +3 -1
  8. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  9. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +2 -5
  10. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  11. package/dist/components/form/fields/template/TemplateField.js +1 -1
  12. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  13. package/dist/helpers/choiceChip.d.ts +98 -0
  14. package/dist/helpers/choiceChip.d.ts.map +1 -0
  15. package/dist/helpers/choiceChip.js +181 -0
  16. package/dist/helpers/choiceChip.js.map +1 -0
  17. package/package.json +1 -1
  18. package/src/components/form/engine/CompactRow.tsx +1 -1
  19. package/src/components/form/engine/FormEngine.stories.tsx +195 -0
  20. package/src/components/form/fields/rich-text/RichText.stories.tsx +178 -1
  21. package/src/components/form/fields/rich-text/RichText.tsx +180 -45
  22. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -5
  23. package/src/components/form/fields/template/TemplateField.tsx +1 -1
  24. package/src/helpers/choiceChip.ts +227 -0
@@ -0,0 +1,227 @@
1
+ // Copyright 2026 Qore Technologies, s.r.o.
2
+ import { IQorusAllowedValue, TQorusFormFieldSchema } from '@qoretechnologies/ts-toolkit';
3
+ import { Editor, Node, Path, Range, Transforms } from 'slate';
4
+ import { findAllowedValueOption } from './allowedValues';
5
+ import { getListElementValue } from './options';
6
+
7
+ /**
8
+ * A value that IS one of the field's own choices, drawn as that choice.
9
+ *
10
+ * A rich-text field offered choices picks one by writing its value, so an
11
+ * author who chose the Slack channel "general" read `C07ERBFS0P6` in the
12
+ * editor. The value is right — it is what the server resolves — but it is not
13
+ * what was chosen by. These helpers draw such a value as one chip named from
14
+ * the choice, the way a template reference is drawn from the catalogue, while
15
+ * what is stored stays the plain value: the chip's own `value` is the choice's,
16
+ * so the text a document stands for is unchanged, and a document that is
17
+ * nothing but the chip is read back as the value it draws.
18
+ */
19
+
20
+ /** The marker a choice chip carries, so it is told apart from a template reference. */
21
+ export const CHOICE_CHIP_META = 'allowedValue';
22
+
23
+ export interface IChoiceChipNode {
24
+ type: 'tag';
25
+ value: string;
26
+ label: string;
27
+ metadata: { [CHOICE_CHIP_META]: true; description?: string };
28
+ children: [{ text: '' }];
29
+ }
30
+
31
+ /** The choice's value as the plain string it is; it arrives in the form engine's `{type, value}` envelope. */
32
+ export const choiceValue = (choice: IQorusAllowedValue): string =>
33
+ String(getListElementValue(choice.value) ?? '');
34
+
35
+ /**
36
+ * The text a document is, when it is one paragraph of plain text and nothing
37
+ * else; `undefined` for anything more — a chip, a mark, a second paragraph.
38
+ */
39
+ export const plainTextOfNodes = (nodes: unknown): string | undefined => {
40
+ if (!Array.isArray(nodes) || nodes.length !== 1) {
41
+ return undefined;
42
+ }
43
+
44
+ const paragraph = nodes[0] as { type?: unknown; children?: unknown[] } | undefined;
45
+
46
+ if (paragraph?.type !== 'paragraph' || !Array.isArray(paragraph.children)) {
47
+ return undefined;
48
+ }
49
+
50
+ let text = '';
51
+
52
+ for (const child of paragraph.children as { text?: unknown }[]) {
53
+ if (typeof child?.text !== 'string' || Object.keys(child).some((key) => key !== 'text')) {
54
+ return undefined;
55
+ }
56
+ text += child.text;
57
+ }
58
+
59
+ return text;
60
+ };
61
+
62
+ /**
63
+ * The choice a value is: a whole plain string the field offers, or a document
64
+ * that is one paragraph of exactly that text — which is what an edit of the
65
+ * chip stores, so an id typed back is the choice it was. A string or document
66
+ * holding anything more is the author's own text and is never a choice.
67
+ */
68
+ export const choiceForValue = (
69
+ value: unknown,
70
+ choices?: IQorusAllowedValue[]
71
+ ): IQorusAllowedValue | undefined => {
72
+ const text = typeof value === 'string' ? value : plainTextOfNodes(value);
73
+
74
+ if (!text || !choices?.length) {
75
+ return undefined;
76
+ }
77
+
78
+ return findAllowedValueOption(text, { allowed_values: choices } as TQorusFormFieldSchema) as
79
+ | IQorusAllowedValue
80
+ | undefined;
81
+ };
82
+
83
+ /** The one-chip document that draws a choice by its name. */
84
+ export const choiceToNodes = (
85
+ choice: IQorusAllowedValue
86
+ ): { type: 'paragraph'; children: ({ text: string } | IChoiceChipNode)[] }[] => {
87
+ const value = choiceValue(choice);
88
+ const description = (choice as { desc?: string; short_desc?: string }).short_desc ?? choice.desc;
89
+
90
+ return [
91
+ {
92
+ type: 'paragraph',
93
+ children: [
94
+ { text: '' },
95
+ {
96
+ type: 'tag',
97
+ value,
98
+ label: choice.display_name ?? value,
99
+ metadata: { [CHOICE_CHIP_META]: true, ...(description ? { description } : {}) },
100
+ children: [{ text: '' }],
101
+ },
102
+ { text: '' },
103
+ ],
104
+ },
105
+ ];
106
+ };
107
+
108
+ export const isChoiceChip = (node: unknown): node is IChoiceChipNode =>
109
+ !!node &&
110
+ typeof node === 'object' &&
111
+ (node as { type?: unknown }).type === 'tag' &&
112
+ !!(node as { metadata?: Record<string, unknown> }).metadata?.[CHOICE_CHIP_META];
113
+
114
+ /**
115
+ * The value a document is, when the document is one choice chip and nothing
116
+ * else; `undefined` when the author has written anything around it, in which
117
+ * case the document itself is the value.
118
+ */
119
+ export const choiceValueOfNodes = (nodes: unknown): string | undefined => {
120
+ if (!Array.isArray(nodes) || nodes.length !== 1) {
121
+ return undefined;
122
+ }
123
+
124
+ const children: unknown[] = (nodes[0] as { children?: unknown[] })?.children ?? [];
125
+ const chips = children.filter(isChoiceChip);
126
+ const text = children
127
+ .filter((node) => !isChoiceChip(node))
128
+ .map((node) => (typeof (node as { text?: unknown })?.text === 'string' ? (node as { text: string }).text : '\u0000'))
129
+ .join('');
130
+
131
+ return chips.length === 1 && text === '' ? chips[0].value : undefined;
132
+ };
133
+
134
+ /**
135
+ * What a choice chip does when the author acts on it, by action.
136
+ *
137
+ * The chip is a void: it holds no text of its own, so a key or a click on it
138
+ * has nothing to land in, and an author who wants to change one letter of the
139
+ * value it draws (`C07ERBFS0P6` to `C07ERBFS0P7`) has no way in unless the
140
+ * chip opens. Each action is configurable, and the default is the one that
141
+ * opens it: a creatable choice is the author's to edit.
142
+ */
143
+ export interface IChoiceChipBehavior {
144
+ /**
145
+ * Backspace with the cursor right after the chip. `unwrap` (the default)
146
+ * turns the chip into its value as plain text, the cursor at the end of it,
147
+ * for the author to edit; `remove` deletes it whole, as the editor deletes
148
+ * any other chip.
149
+ */
150
+ backspace?: 'unwrap' | 'remove';
151
+ /**
152
+ * A click on the chip's body. `unwrap` (the default) turns the chip into its
153
+ * value as plain text, the cursor at the end of it; `cursor` only places the
154
+ * cursor after it, as a click on a template chip does.
155
+ */
156
+ click?: 'unwrap' | 'cursor';
157
+ }
158
+
159
+ export const DEFAULT_CHOICE_CHIP_BEHAVIOR: Required<IChoiceChipBehavior> = {
160
+ backspace: 'unwrap',
161
+ click: 'unwrap',
162
+ };
163
+
164
+ /** Where the node is in the document, by identity; the editor renders the document's own nodes, so a chip handed to a click handler is one of them. */
165
+ export const pathOfNode = (editor: Editor, node: unknown): Path | undefined => {
166
+ // Stepped by hand: the build target cannot iterate a generator (TS2802).
167
+ const nodes = Node.nodes(editor);
168
+
169
+ for (let entry = nodes.next(); !entry.done; entry = nodes.next()) {
170
+ if (entry.value[0] === node) {
171
+ return entry.value[1];
172
+ }
173
+ }
174
+
175
+ return undefined;
176
+ };
177
+
178
+ /**
179
+ * The chip the cursor is on: right after it with nothing between, where
180
+ * Backspace would take it, or inside it, where the arrow keys can leave the
181
+ * cursor. Nothing when the selection spans text or sits anywhere else.
182
+ */
183
+ export const choiceChipAtCursor = (editor: Editor): Path | undefined => {
184
+ const { selection } = editor;
185
+
186
+ if (!selection || !Range.isCollapsed(selection)) {
187
+ return undefined;
188
+ }
189
+
190
+ const { path, offset } = selection.anchor;
191
+ const inside = Editor.above(editor, { at: path, match: isChoiceChip, voids: true });
192
+
193
+ if (inside) {
194
+ return inside[1];
195
+ }
196
+
197
+ if (offset !== 0 || !Path.hasPrevious(path)) {
198
+ return undefined;
199
+ }
200
+
201
+ const previous = Path.previous(path);
202
+
203
+ return isChoiceChip(Node.get(editor, previous)) ? previous : undefined;
204
+ };
205
+
206
+ /**
207
+ * Opens the chip at `path` into its value as plain text, the cursor at the
208
+ * end of the text: what the author read is what they chose, now editable
209
+ * letter by letter. The document is text from here on, and is stored as the
210
+ * field stores anything typed; it is drawn as a chip again only when it is
211
+ * read back as exactly one of the choices.
212
+ */
213
+ export const unwrapChoiceChip = (editor: Editor, path: Path): boolean => {
214
+ const chip = Node.get(editor, path);
215
+
216
+ if (!isChoiceChip(chip)) {
217
+ return false;
218
+ }
219
+
220
+ Editor.withoutNormalizing(editor, () => {
221
+ Transforms.removeNodes(editor, { at: path });
222
+ Transforms.insertNodes(editor, [{ text: chip.value }], { at: path });
223
+ Transforms.select(editor, Editor.end(editor, path));
224
+ });
225
+
226
+ return true;
227
+ };