@qoretechnologies/reqraft 0.10.61 → 0.10.62-pr.137.g86641aa
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/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.js +8 -3
- package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
- package/dist/components/dpqlEditor/types.d.ts +5 -0
- package/dist/components/dpqlEditor/types.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +17 -8
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +65 -15
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +12 -1
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +2 -0
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +5 -1
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/expressions/ExpressionField.d.ts +21 -1
- package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
- package/dist/components/form/expressions/ExpressionField.js +72 -14
- package/dist/components/form/expressions/ExpressionField.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +16 -3
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/expressionOperands.d.ts +16 -0
- package/dist/components/form/expressions/expressionOperands.d.ts.map +1 -0
- package/dist/components/form/expressions/expressionOperands.js +97 -0
- package/dist/components/form/expressions/expressionOperands.js.map +1 -0
- package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/mockExpressions.js +15 -0
- package/dist/components/form/expressions/mockExpressions.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts +2 -0
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +24 -5
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/expressions/textOfExpression.d.ts +18 -0
- package/dist/components/form/expressions/textOfExpression.d.ts.map +1 -0
- package/dist/components/form/expressions/textOfExpression.js +37 -0
- package/dist/components/form/expressions/textOfExpression.js.map +1 -0
- package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.js +7 -7
- package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +8 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +10 -10
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts +6 -0
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +280 -55
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/writtenAsText.d.ts +57 -0
- package/dist/components/form/fields/template/writtenAsText.d.ts.map +1 -0
- package/dist/components/form/fields/template/writtenAsText.js +203 -0
- package/dist/components/form/fields/template/writtenAsText.js.map +1 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +7 -1
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts +6 -0
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +7 -6
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/helpers/templates.d.ts +18 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +30 -1
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +98 -18
- package/dist/helpers/validations.js.map +1 -1
- package/package.json +1 -1
- package/src/components/dpqlEditor/DpqlEditor.tsx +11 -18
- package/src/components/dpqlEditor/types.ts +5 -0
- package/src/components/form/engine/CompactRow.tsx +31 -10
- package/src/components/form/engine/FormEngine.stories.tsx +656 -18
- package/src/components/form/engine/FormEngine.tsx +64 -22
- package/src/components/form/engine/FormEngineRemote.stories.tsx +5 -0
- package/src/components/form/engine/OptionFieldMessages.tsx +12 -1
- package/src/components/form/engine/compactRowContext.ts +2 -0
- package/src/components/form/engine/readFirst.ts +7 -1
- package/src/components/form/expressions/ExpressionField.stories.tsx +57 -9
- package/src/components/form/expressions/ExpressionField.tsx +179 -60
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +8 -1
- package/src/components/form/expressions/builder/index.tsx +16 -3
- package/src/components/form/expressions/dpqlMockLanguage.ts +7 -2
- package/src/components/form/expressions/expressionOperands.ts +74 -0
- package/src/components/form/expressions/mockExpressions.ts +16 -4
- package/src/components/form/expressions/renderExpressionToText.ts +26 -9
- package/src/components/form/expressions/textOfExpression.ts +39 -0
- package/src/components/form/expressions/useRenderExpression.ts +9 -1
- package/src/components/form/fields/number/Number.stories.tsx +8 -7
- package/src/components/form/fields/rich-text/RichText.tsx +18 -10
- package/src/components/form/fields/template/TemplateField.stories.tsx +486 -18
- package/src/components/form/fields/template/TemplateField.tsx +506 -62
- package/src/components/form/fields/template/writtenAsText.ts +217 -0
- package/src/components/form/index.tsx +6 -0
- package/src/components/smartEditor/useLspSession.ts +15 -10
- package/src/helpers/templates.ts +35 -0
- package/src/helpers/validations.ts +83 -12
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Copyright 2026 Qore Technologies, s.r.o.
|
|
2
|
+
// What a field holds for the text written in its template editor.
|
|
3
|
+
//
|
|
4
|
+
// A field that takes templates and custom values shows ONE editor for both: a text
|
|
5
|
+
// field where a chosen template is a chip, with text typed around it (qorus#646). The
|
|
6
|
+
// field's value is read from that text:
|
|
7
|
+
//
|
|
8
|
+
// - a template standing alone is that template - in an untyped field, of the
|
|
9
|
+
// template's own type (a whole-number field of the record makes the value a whole
|
|
10
|
+
// number);
|
|
11
|
+
// - a literal of a scalar field's type (`12` in a whole-number field, `true` in a
|
|
12
|
+
// yes/no one) is that literal;
|
|
13
|
+
// - null in an untyped field is written `null` (an empty field holds no value at all);
|
|
14
|
+
// - anything else is text: an untyped field's value becomes text, a typed field holds
|
|
15
|
+
// what was written and says it does not fit (an expression in it is detected and
|
|
16
|
+
// offered, or switched to, by the field's DPQL detection).
|
|
17
|
+
//
|
|
18
|
+
// Deleting the text around a template makes it a lone template again, and the value
|
|
19
|
+
// takes the template's type again. Pure: no React, no transport.
|
|
20
|
+
import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
|
|
21
|
+
import { isUntypedOptionType } from '../../../../helpers/optionUiTypes';
|
|
22
|
+
import {
|
|
23
|
+
findTemplate,
|
|
24
|
+
isCompleteTemplateToken,
|
|
25
|
+
isValueTemplate,
|
|
26
|
+
TEMPLATE_TOKEN_SOURCE,
|
|
27
|
+
} from '../../../../helpers/templates';
|
|
28
|
+
|
|
29
|
+
/** Scalar types whose value can be written as text: whole numbers, numbers, yes/no. Not a date, which has
|
|
30
|
+
* its own control. */
|
|
31
|
+
const TEXTABLE_SCALARS: Record<string, 'int' | 'number' | 'bool'> = {
|
|
32
|
+
int: 'int',
|
|
33
|
+
integer: 'int',
|
|
34
|
+
softint: 'int',
|
|
35
|
+
number: 'number',
|
|
36
|
+
float: 'number',
|
|
37
|
+
softfloat: 'number',
|
|
38
|
+
softnumber: 'number',
|
|
39
|
+
bool: 'bool',
|
|
40
|
+
boolean: 'bool',
|
|
41
|
+
softbool: 'bool',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Whether a field of this type is written in the template editor: text, untyped, or a textable scalar. */
|
|
45
|
+
export const isWrittenAsText = (type?: string): boolean =>
|
|
46
|
+
type === 'string' || isUntypedOptionType(type) || (!!type && type in TEXTABLE_SCALARS);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a form option of this type is written as text on its Value tab - a text field taking templates as
|
|
50
|
+
* chips and detecting expressions - rather than on its own control (qorus#646, David): text, untyped values,
|
|
51
|
+
* whole numbers and numbers. A yes / no, a date and fixed choices keep their own control.
|
|
52
|
+
*/
|
|
53
|
+
export const isWrittenAsTextOnTheValueTab = (type?: string): boolean =>
|
|
54
|
+
type === 'string' ||
|
|
55
|
+
isUntypedOptionType(type) ||
|
|
56
|
+
(!!type && (TEXTABLE_SCALARS[type] === 'int' || TEXTABLE_SCALARS[type] === 'number'));
|
|
57
|
+
|
|
58
|
+
/** The tabs a field's value is entered on (see `valueTabsOf`). */
|
|
59
|
+
export type TValueTab = 'value' | 'expression' | 'visual' | 'template';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The text a value shows as on the Value tab: text as it is, a number or a yes/no as written, null as
|
|
63
|
+
* `null` (in an untyped field), nothing for no value.
|
|
64
|
+
*/
|
|
65
|
+
export const valueTextOf = (value: unknown, type?: string): string => {
|
|
66
|
+
if (value === undefined) return '';
|
|
67
|
+
if (value === null) return isUntypedOptionType(type) ? NULL_TEXT : '';
|
|
68
|
+
if (typeof value === 'string') return value;
|
|
69
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
70
|
+
return '';
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A value written as an expression's text: a literal as DPQL writes it (`12`, `true`, `"open"`), a template
|
|
75
|
+
* as itself, text with templates as the text and the templates joined (`concat("SUP-", $record:{pos})`).
|
|
76
|
+
* The Expression tab opens on this for a value that is not an expression yet.
|
|
77
|
+
*/
|
|
78
|
+
export const expressionTextOfValue = (value: unknown): string => {
|
|
79
|
+
if (value === undefined || value === null) return value === null ? 'null' : '';
|
|
80
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
81
|
+
if (typeof value !== 'string') return '';
|
|
82
|
+
const text = String(value);
|
|
83
|
+
if (isCompleteTemplateToken(text)) return String(text);
|
|
84
|
+
const plain: string = text;
|
|
85
|
+
const parts = plain.split(new RegExp(`(${TEMPLATE_TOKEN_SOURCE})`)).filter((part) => part !== '');
|
|
86
|
+
if (parts.length === 1) return JSON.stringify(plain);
|
|
87
|
+
return `concat(${parts.map((part) => (isCompleteTemplateToken(part) ? part : JSON.stringify(part))).join(', ')})`;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A lone template written in the expression editor's Text view, as the server parses it: the `template`
|
|
92
|
+
* operation with the reference as its one argument. It is stored as the bare template, which every consumer
|
|
93
|
+
* evaluates; the expression form of it is not one the server reads back as the template's value.
|
|
94
|
+
*/
|
|
95
|
+
export const loneTemplateOf = (expression: unknown): string | undefined => {
|
|
96
|
+
const node = (expression as { value?: { exp?: string; args?: unknown[] } } | undefined)?.value;
|
|
97
|
+
if (node?.exp !== 'template' || node.args?.length !== 1) return undefined;
|
|
98
|
+
const arg = node.args[0] as { value?: { raw?: unknown } | string } | undefined;
|
|
99
|
+
const raw = typeof arg?.value === 'string' ? arg.value : arg?.value?.raw;
|
|
100
|
+
return typeof raw === 'string' && isCompleteTemplateToken(raw) ? raw : undefined;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A lone value written in the expression editor's Text view (`12`, `true`, `"open"`), as the server parses
|
|
105
|
+
* it: the `value` operation with a plain value as its one argument. It is stored as that value - the field's
|
|
106
|
+
* own shape - rather than as an expression the Visual view has nothing to show for. A record field is not
|
|
107
|
+
* such a value: it is read from the row.
|
|
108
|
+
*/
|
|
109
|
+
export const loneValueOf = (expression: unknown): { value: unknown } | undefined => {
|
|
110
|
+
const node = (expression as { value?: { exp?: string; args?: unknown[] } } | undefined)?.value;
|
|
111
|
+
if (node?.exp !== 'value' || node.args?.length !== 1) return undefined;
|
|
112
|
+
const arg = node.args[0] as { value?: unknown; is_expression?: boolean } | undefined;
|
|
113
|
+
const value = arg?.value;
|
|
114
|
+
if (!arg || arg.is_expression || (value !== null && typeof value === 'object')) return undefined;
|
|
115
|
+
if (typeof value === 'string' && isValueTemplate(value)) return undefined;
|
|
116
|
+
return { value };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** The text a value is seeded into the Text view as: a template, a number or a yes/no as written. */
|
|
120
|
+
export const expressionSeedOf = (value: unknown): string => {
|
|
121
|
+
if (typeof value === 'string') return isCompleteTemplateToken(value) ? value : '';
|
|
122
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
123
|
+
return '';
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const INT_LITERAL = /^\s*[-+]?\d+\s*$/;
|
|
127
|
+
const NUMBER_LITERAL = /^\s*[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?\s*$/i;
|
|
128
|
+
const BOOL_LITERAL = /^\s*(true|false)\s*$/i;
|
|
129
|
+
|
|
130
|
+
/** The literal `text` writes in a scalar type, or `undefined` when it writes none. */
|
|
131
|
+
export const scalarLiteral = (text: string, type?: string): number | boolean | undefined => {
|
|
132
|
+
switch (type ? TEXTABLE_SCALARS[type] : undefined) {
|
|
133
|
+
case 'int':
|
|
134
|
+
return INT_LITERAL.test(text) ? Number.parseInt(text, 10) : undefined;
|
|
135
|
+
case 'number':
|
|
136
|
+
return NUMBER_LITERAL.test(text) ? Number.parseFloat(text) : undefined;
|
|
137
|
+
case 'bool': {
|
|
138
|
+
const match = BOOL_LITERAL.exec(text);
|
|
139
|
+
return match ? match[1].toLowerCase() === 'true' : undefined;
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** How null is written in an untyped field: the word, so it is not taken for an empty field. */
|
|
147
|
+
export const NULL_TEXT = 'null';
|
|
148
|
+
|
|
149
|
+
/** The text an untyped field shows for its value: `null` for null; for anything else, the text it is. */
|
|
150
|
+
export const untypedTextOf = (value: unknown, type?: string): string | undefined =>
|
|
151
|
+
value === null && isUntypedOptionType(type) ? NULL_TEXT : undefined;
|
|
152
|
+
|
|
153
|
+
export interface ITemplateTextValue {
|
|
154
|
+
value: unknown;
|
|
155
|
+
/** the value's type: the field's own, or for an untyped field the type the text gives it */
|
|
156
|
+
type?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** What the field holds for `text`, written in its template editor. See the top of this file. */
|
|
160
|
+
export const templateTextValue = (
|
|
161
|
+
text: string,
|
|
162
|
+
type: string | undefined,
|
|
163
|
+
templates?: IReqoreFormTemplates
|
|
164
|
+
): ITemplateTextValue => {
|
|
165
|
+
const trimmed = text.trim();
|
|
166
|
+
if (isCompleteTemplateToken(trimmed)) {
|
|
167
|
+
if (isUntypedOptionType(type)) {
|
|
168
|
+
const badge = templates ? findTemplate(templates, trimmed)?.badge : undefined;
|
|
169
|
+
return { value: trimmed, type: typeof badge === 'string' && badge ? badge : type };
|
|
170
|
+
}
|
|
171
|
+
return { value: trimmed, type };
|
|
172
|
+
}
|
|
173
|
+
// null, the literal that says "no value", is a value of an untyped field: written `null`, it is null
|
|
174
|
+
if (isUntypedOptionType(type) && trimmed === NULL_TEXT) {
|
|
175
|
+
return { value: null, type };
|
|
176
|
+
}
|
|
177
|
+
const literal = scalarLiteral(text, type);
|
|
178
|
+
if (literal !== undefined) {
|
|
179
|
+
return { value: literal, type };
|
|
180
|
+
}
|
|
181
|
+
return { value: text, type: isUntypedOptionType(type) ? 'string' : type };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** The kind of value a type holds, for telling whether a template and a field hold the same kind. */
|
|
185
|
+
const valueKindOf = (type?: string): string | undefined => {
|
|
186
|
+
if (!type) return undefined;
|
|
187
|
+
const t = type.replace(/^\*/, '').replace(/^soft/, '');
|
|
188
|
+
if (TEXTABLE_SCALARS[t]) return TEXTABLE_SCALARS[t];
|
|
189
|
+
if (t === 'string' || t === 'date' || t === 'list' || t === 'hash' || t === 'binary') return t;
|
|
190
|
+
return undefined;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Whether a template of this type is a value of the field's type as it is, with nothing to convert. */
|
|
194
|
+
export const sameValueType = (templateType: string, fieldType?: string): boolean => {
|
|
195
|
+
const a = valueKindOf(templateType);
|
|
196
|
+
const b = valueKindOf(fieldType);
|
|
197
|
+
// a whole number is a number too
|
|
198
|
+
return !a || !b || a === b || (a === 'int' && b === 'number');
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** A type in words: "a whole number", "text". */
|
|
202
|
+
export const templateTypeName = (type?: string): string => {
|
|
203
|
+
switch (valueKindOf(type)) {
|
|
204
|
+
case 'int':
|
|
205
|
+
return 'A whole number';
|
|
206
|
+
case 'number':
|
|
207
|
+
return 'A number';
|
|
208
|
+
case 'bool':
|
|
209
|
+
return 'A yes / no value';
|
|
210
|
+
case 'string':
|
|
211
|
+
return 'Text';
|
|
212
|
+
case 'date':
|
|
213
|
+
return 'A date';
|
|
214
|
+
default:
|
|
215
|
+
return type ?? 'A value';
|
|
216
|
+
}
|
|
217
|
+
};
|
|
@@ -35,6 +35,12 @@ export * from './expressions/useExpressions';
|
|
|
35
35
|
// Named (not wildcard) so the `_resetRenderExpressionTransportForTests`
|
|
36
36
|
// test hook stays out of the public package surface.
|
|
37
37
|
export { renderExpressionToText, useRenderExpression } from './expressions/useRenderExpression';
|
|
38
|
+
export {
|
|
39
|
+
expressionOperand,
|
|
40
|
+
expressionWithOperands,
|
|
41
|
+
isExpressionComplete,
|
|
42
|
+
} from './expressions/expressionOperands';
|
|
43
|
+
export { EXPRESSION_HOLE } from './expressions/renderExpressionToText';
|
|
38
44
|
export type {
|
|
39
45
|
IRenderedExpression,
|
|
40
46
|
IUseRenderExpressionResult,
|
|
@@ -40,6 +40,12 @@ export interface IUseLspSessionResult {
|
|
|
40
40
|
uri: string;
|
|
41
41
|
/** Becomes `true` once `initialize` + `didOpen` complete. */
|
|
42
42
|
isReady: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* `true` once connecting has FAILED: there is no language server to talk to (none configured, or it
|
|
45
|
+
* cannot be reached). A host that needs one - the expression editor's Text view - shows what works
|
|
46
|
+
* without it instead of waiting on a session that is not coming.
|
|
47
|
+
*/
|
|
48
|
+
isUnavailable: boolean;
|
|
43
49
|
/**
|
|
44
50
|
* Becomes `true` once any language-specific context binding the
|
|
45
51
|
* wrapper requires has resolved. For the generic primitive session
|
|
@@ -93,9 +99,7 @@ export interface IUseLspSessionResult {
|
|
|
93
99
|
format: () => Promise<string | null>;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
export function useLspSession(
|
|
97
|
-
options: IUseLspSessionOptions
|
|
98
|
-
): IUseLspSessionResult {
|
|
102
|
+
export function useLspSession(options: IUseLspSessionOptions): IUseLspSessionResult {
|
|
99
103
|
const { languageId, uri: uriProp, initialMetadata, url, initialText = '' } = options;
|
|
100
104
|
|
|
101
105
|
const clientRef = useRef<ReqraftLspClient | null>(null);
|
|
@@ -111,11 +115,12 @@ export function useLspSession(
|
|
|
111
115
|
|
|
112
116
|
const [client, setClient] = useState<ReqraftLspClient | null>(null);
|
|
113
117
|
const [isReady, setIsReady] = useState(false);
|
|
118
|
+
const [isUnavailable, setIsUnavailable] = useState(false);
|
|
114
119
|
const [diagnostics, setDiagnostics] = useState<ILspDiagnostic[]>([]);
|
|
115
|
-
const [semanticTokensLegend, setSemanticTokensLegend] =
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
const [semanticTokensLegend, setSemanticTokensLegend] = useState<ILspSemanticTokensLegend | null>(
|
|
121
|
+
null
|
|
122
|
+
);
|
|
123
|
+
const [capabilities, setCapabilities] = useState<ILspServerCapabilities | null>(null);
|
|
119
124
|
|
|
120
125
|
useEffect(() => {
|
|
121
126
|
const c = new ReqraftLspClient({
|
|
@@ -147,9 +152,7 @@ export function useLspSession(
|
|
|
147
152
|
serverTextRef.current = editorTextRef.current;
|
|
148
153
|
c.didOpen(
|
|
149
154
|
editorTextRef.current,
|
|
150
|
-
initialMetadata && Object.keys(initialMetadata).length > 0
|
|
151
|
-
? initialMetadata
|
|
152
|
-
: undefined
|
|
155
|
+
initialMetadata && Object.keys(initialMetadata).length > 0 ? initialMetadata : undefined
|
|
153
156
|
);
|
|
154
157
|
setIsReady(true);
|
|
155
158
|
})
|
|
@@ -162,6 +165,7 @@ export function useLspSession(
|
|
|
162
165
|
// eslint-disable-next-line no-console
|
|
163
166
|
console.error('[LSP session] Connect failed:', err);
|
|
164
167
|
setIsReady(false);
|
|
168
|
+
setIsUnavailable(true);
|
|
165
169
|
});
|
|
166
170
|
|
|
167
171
|
return () => {
|
|
@@ -217,6 +221,7 @@ export function useLspSession(
|
|
|
217
221
|
client,
|
|
218
222
|
uri: uriRef.current,
|
|
219
223
|
isReady,
|
|
224
|
+
isUnavailable,
|
|
220
225
|
// Generic session has no language-specific context to bind;
|
|
221
226
|
// wrappers (`useDpqlSession`) override this on their returned
|
|
222
227
|
// session by spreading + replacing.
|
package/src/helpers/templates.ts
CHANGED
|
@@ -94,6 +94,41 @@ const COMPLETE_TEMPLATE_TOKEN = new RegExp(`^${TEMPLATE_TOKEN_SOURCE}$`);
|
|
|
94
94
|
export const isCompleteTemplateToken = (value?: unknown): value is string =>
|
|
95
95
|
typeof value === 'string' && COMPLETE_TEMPLATE_TOKEN.test(value);
|
|
96
96
|
|
|
97
|
+
const TEMPLATE_TOKEN_ANYWHERE = new RegExp(TEMPLATE_TOKEN_SOURCE);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Text with a template in it that is not the template alone: `$record:{pos} Stk.`, `SUP-$record:{pos}`,
|
|
101
|
+
* two templates side by side. In text it is text; a whole number, a number, a yes/no or a date cannot
|
|
102
|
+
* hold it (qorus#646).
|
|
103
|
+
*/
|
|
104
|
+
export const hasTextAroundATemplate = (value?: unknown): value is string =>
|
|
105
|
+
typeof value === 'string' && TEMPLATE_TOKEN_ANYWHERE.test(value) && !isCompleteTemplateToken(value);
|
|
106
|
+
|
|
107
|
+
/** The text around the templates in a value that is not one template alone, and how many templates it holds. */
|
|
108
|
+
export interface ITextAroundATemplate {
|
|
109
|
+
/** The pieces of text before, between and after the templates, trimmed; empty pieces left out. */
|
|
110
|
+
text: string[];
|
|
111
|
+
templates: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* What makes a value text rather than one template alone: `$record:{pos} Stk.` holds the text "Stk." and
|
|
116
|
+
* one template; `$record:{a}$record:{b}` no text and two. `undefined` where the value is not text with a
|
|
117
|
+
* template in it, or is one template alone (see {@link hasTextAroundATemplate}).
|
|
118
|
+
*/
|
|
119
|
+
export const textAroundATemplate = (value?: unknown): ITextAroundATemplate | undefined => {
|
|
120
|
+
if (!hasTextAroundATemplate(value)) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const tokens = new RegExp(TEMPLATE_TOKEN_SOURCE, 'g');
|
|
124
|
+
const templates = value.match(tokens)?.length ?? 0;
|
|
125
|
+
const text = value
|
|
126
|
+
.split(tokens)
|
|
127
|
+
.map((piece) => piece.trim())
|
|
128
|
+
.filter((piece) => piece !== '');
|
|
129
|
+
return { text, templates };
|
|
130
|
+
};
|
|
131
|
+
|
|
97
132
|
/** A complete token carrying at least one BRACED context segment —
|
|
98
133
|
* `$data:{W2n….filename}`, `$qore-expr:{1 + 2}`. These are machine-written
|
|
99
134
|
* references nobody types by hand, so a surface may render them as a picker
|
|
@@ -2,7 +2,7 @@ import { isValidSixCharHex } from '@qoretechnologies/reqore/dist/helpers/colors'
|
|
|
2
2
|
import { IQorusFormSchema, TQorusForm, TQorusFormFieldSchema } from '@qoretechnologies/ts-toolkit';
|
|
3
3
|
import { isValidCron } from 'cron-validator';
|
|
4
4
|
import jsyaml from 'js-yaml';
|
|
5
|
-
import { isBoolean, isNull, isString, isUndefined, memoize, omit } from 'lodash';
|
|
5
|
+
import { isBoolean, isNull, isString, isUndefined, memoize, omit, upperFirst } from 'lodash';
|
|
6
6
|
import every from 'lodash/every';
|
|
7
7
|
import isArray from 'lodash/isArray';
|
|
8
8
|
import isDate from 'lodash/isDate';
|
|
@@ -15,7 +15,7 @@ import { fixOperatorValue, getAddress, getProtocol, splitByteSize } from './comm
|
|
|
15
15
|
import { isOptionInterfaceUiType } from './optionUiTypes';
|
|
16
16
|
import { getListElementValue, getOptionsFromRequiredGroups } from './options';
|
|
17
17
|
import { IProviderType, TVariableActionValue, maybeBuildOptionProvider } from './providerValue';
|
|
18
|
-
import { getTemplateKey, getTemplateValue, isValueTemplate } from './templates';
|
|
18
|
+
import { getTemplateKey, getTemplateValue, isValueTemplate, textAroundATemplate } from './templates';
|
|
19
19
|
import { getUnlistedChoiceReason } from './allowedValues';
|
|
20
20
|
|
|
21
21
|
/** The five cron fields, in order, as a schedule hash may name them. */
|
|
@@ -244,6 +244,56 @@ const isValueDefined = (type: string, value: any): boolean => {
|
|
|
244
244
|
return value !== undefined && value !== '';
|
|
245
245
|
};
|
|
246
246
|
|
|
247
|
+
/** What a value of each type that is not text is called, where text around a template makes it text. */
|
|
248
|
+
const TEXT_AROUND_A_TEMPLATE_NOT_A: Record<string, string> = (() => {
|
|
249
|
+
const wholeNumber = 'a whole number';
|
|
250
|
+
const number = 'a number';
|
|
251
|
+
const yesNo = 'true or false';
|
|
252
|
+
const date = 'a date';
|
|
253
|
+
return {
|
|
254
|
+
int: wholeNumber,
|
|
255
|
+
integer: wholeNumber,
|
|
256
|
+
softint: wholeNumber,
|
|
257
|
+
number,
|
|
258
|
+
float: number,
|
|
259
|
+
softfloat: number,
|
|
260
|
+
softnumber: number,
|
|
261
|
+
bool: yesNo,
|
|
262
|
+
boolean: yesNo,
|
|
263
|
+
softbool: yesNo,
|
|
264
|
+
date,
|
|
265
|
+
softdate: date,
|
|
266
|
+
};
|
|
267
|
+
})();
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* What is said of a value of a type that is not text, written with text around a template: what makes it
|
|
271
|
+
* text, and what to do about it (qlip build 20261008-083118: "How does the user fix this? Why has this
|
|
272
|
+
* happened?"). `undefined` where the type is text, or the value is not text around a template.
|
|
273
|
+
*/
|
|
274
|
+
const textAroundATemplateReason = (type: string, value: unknown): string | undefined => {
|
|
275
|
+
const notA = TEXT_AROUND_A_TEMPLATE_NOT_A[type];
|
|
276
|
+
const around = notA ? textAroundATemplate(value) : undefined;
|
|
277
|
+
if (!around) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
if (around.templates > 1) {
|
|
281
|
+
return around.text.length
|
|
282
|
+
? `Text and more than one template make this text, not ${notA}. Keep one template alone.`
|
|
283
|
+
: `Templates side by side make this text, not ${notA}. Keep one of them alone.`;
|
|
284
|
+
}
|
|
285
|
+
return `"${around.text.join(' … ')}" makes this text, not ${notA}. Delete it to keep the template alone.`;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/** 1st, 2nd, 3rd, 4th, … 11th, 12th, 13th, … 21st: a position as it is written. */
|
|
289
|
+
const ordinal = (n: number): string => {
|
|
290
|
+
const tens = n % 100;
|
|
291
|
+
if (tens >= 11 && tens <= 13) {
|
|
292
|
+
return `${n}th`;
|
|
293
|
+
}
|
|
294
|
+
return `${n}${({ 1: 'st', 2: 'nd', 3: 'rd' } as Record<number, string>)[n % 10] ?? 'th'}`;
|
|
295
|
+
};
|
|
296
|
+
|
|
247
297
|
export const _validateField = (
|
|
248
298
|
type: string,
|
|
249
299
|
value?: any,
|
|
@@ -272,6 +322,14 @@ export const _validateField = (
|
|
|
272
322
|
if (pos > 0) {
|
|
273
323
|
type = type.slice(0, pos);
|
|
274
324
|
}
|
|
325
|
+
/* A value whose type is not text, written with text around a template, is not of that type: a whole
|
|
326
|
+
number written as "$record:{pos} Stk." passed as a template - anything starting with `$` and holding
|
|
327
|
+
a `:` did - and the form could be saved with a value its type cannot hold (qorus#646). A template
|
|
328
|
+
alone, a literal and an expression are judged as before. */
|
|
329
|
+
const textAroundReason = textAroundATemplateReason(type, value);
|
|
330
|
+
if (textAroundReason) {
|
|
331
|
+
return invalidResult(textAroundReason);
|
|
332
|
+
}
|
|
275
333
|
// Check if the value is a template string
|
|
276
334
|
if (isValueTemplate(value)) {
|
|
277
335
|
const hasKey = !!getTemplateKey(value);
|
|
@@ -1617,6 +1675,14 @@ export const _validateField = (
|
|
|
1617
1675
|
// Now we need to validate each argument, either as a normal value or as a sub-expression
|
|
1618
1676
|
const args = castedValue.value?.args ?? [];
|
|
1619
1677
|
|
|
1678
|
+
// each operand's name, as the editor shows it
|
|
1679
|
+
const argLabels: string[] = args.map((_arg: unknown, index: number) => {
|
|
1680
|
+
const definition = expressionDefinition.varargs
|
|
1681
|
+
? expressionDefinition.args[0]
|
|
1682
|
+
: expressionDefinition.args[index];
|
|
1683
|
+
return definition?.display_name ?? definition?.name ?? `argument ${index + 1}`;
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1620
1686
|
for (let index = 0; index < args.length; index++) {
|
|
1621
1687
|
const argValue: any = args[index];
|
|
1622
1688
|
const argDefinition = expressionDefinition.varargs
|
|
@@ -1627,8 +1693,13 @@ export const _validateField = (
|
|
|
1627
1693
|
`display_name` is what a catalogue MAY carry; the served one carries
|
|
1628
1694
|
`name`, so every message about an argument read "argument 1
|
|
1629
1695
|
("undefined") is invalid" and named a field that does not exist. */
|
|
1630
|
-
const argLabel =
|
|
1631
|
-
|
|
1696
|
+
const argLabel = argLabels[index];
|
|
1697
|
+
/* Which operand, where its name alone does not say: every operand of a call taking any number of
|
|
1698
|
+
them is "Value" (qlip build 20261008-083118: "which "value" is the message mentioning"). Said by
|
|
1699
|
+
its position too, as the editor shows them, left to right. */
|
|
1700
|
+
const operand = argLabels.some((label, other) => other !== index && label === argLabel)
|
|
1701
|
+
? `the ${ordinal(index + 1)} "${argLabel}"`
|
|
1702
|
+
: undefined;
|
|
1632
1703
|
|
|
1633
1704
|
/* An explicit null is a VALUE, and DPQL says so: `null` is a literal
|
|
1634
1705
|
that parses, serializes and round-trips like any other.
|
|
@@ -1659,10 +1730,8 @@ export const _validateField = (
|
|
|
1659
1730
|
});
|
|
1660
1731
|
|
|
1661
1732
|
if (!result.isValid) {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
`Sub-expression for argument ${index + 1} ("${argLabel}") is invalid`
|
|
1665
|
-
);
|
|
1733
|
+
// said by the operand's name, as the editor shows it - not by its position in the call
|
|
1734
|
+
return withContext(result, `The expression in ${operand ?? `"${argLabel}"`} is invalid`);
|
|
1666
1735
|
}
|
|
1667
1736
|
|
|
1668
1737
|
continue;
|
|
@@ -1686,10 +1755,12 @@ export const _validateField = (
|
|
|
1686
1755
|
});
|
|
1687
1756
|
|
|
1688
1757
|
if (!result.isValid) {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1758
|
+
// an operand not filled in yet says what to do; one that is wrong says which and why (David's
|
|
1759
|
+
// review of qorus#646: "Value for argument 1 ("Value") is invalid: Missing value" while building)
|
|
1760
|
+
if (argValue?.value === undefined || argValue?.value === null || argValue?.value === '') {
|
|
1761
|
+
return invalidResult(operand ? `Enter ${operand}` : `Enter a value for "${argLabel}"`);
|
|
1762
|
+
}
|
|
1763
|
+
return withContext(result, operand ? `${upperFirst(operand)} is invalid` : `"${argLabel}" is invalid`);
|
|
1693
1764
|
}
|
|
1694
1765
|
}
|
|
1695
1766
|
|