@bonsae/nrg 0.1.0

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 (70) hide show
  1. package/README.md +130 -0
  2. package/build/server/index.cjs +910 -0
  3. package/build/server/resources/nrg-client.js +6530 -0
  4. package/build/server/resources/vue.esm-browser.prod.js +13 -0
  5. package/build/vite/index.js +1893 -0
  6. package/build/vite/utils.js +60 -0
  7. package/package.json +110 -0
  8. package/src/core/client/api/index.ts +17 -0
  9. package/src/core/client/app.vue +201 -0
  10. package/src/core/client/components/node-red-config-input.vue +57 -0
  11. package/src/core/client/components/node-red-editor-input.vue +283 -0
  12. package/src/core/client/components/node-red-input.vue +71 -0
  13. package/src/core/client/components/node-red-json-schema-form.vue +369 -0
  14. package/src/core/client/components/node-red-select-input.vue +86 -0
  15. package/src/core/client/components/node-red-typed-input.vue +130 -0
  16. package/src/core/client/components.d.ts +18 -0
  17. package/src/core/client/globals.d.ts +17 -0
  18. package/src/core/client/index.ts +504 -0
  19. package/src/core/client/shims-vue.d.ts +5 -0
  20. package/src/core/client/tsconfig.json +18 -0
  21. package/src/core/client/virtual.d.ts +5 -0
  22. package/src/core/constants.ts +18 -0
  23. package/src/core/server/index.ts +209 -0
  24. package/src/core/server/nodes/config-node.ts +67 -0
  25. package/src/core/server/nodes/index.ts +4 -0
  26. package/src/core/server/nodes/io-node.ts +178 -0
  27. package/src/core/server/nodes/node.ts +255 -0
  28. package/src/core/server/nodes/types/config-node.ts +28 -0
  29. package/src/core/server/nodes/types/index.ts +3 -0
  30. package/src/core/server/nodes/types/io-node.ts +37 -0
  31. package/src/core/server/nodes/types/node.ts +41 -0
  32. package/src/core/server/nodes/utils.ts +83 -0
  33. package/src/core/server/schemas/base.ts +66 -0
  34. package/src/core/server/schemas/index.ts +3 -0
  35. package/src/core/server/schemas/type.ts +95 -0
  36. package/src/core/server/schemas/types/index.ts +73 -0
  37. package/src/core/server/tsconfig.json +17 -0
  38. package/src/core/server/types/index.ts +73 -0
  39. package/src/core/server/utils.ts +56 -0
  40. package/src/core/server/validator.ts +32 -0
  41. package/src/core/validator.ts +222 -0
  42. package/src/tsconfig/base.json +23 -0
  43. package/src/tsconfig/client.json +11 -0
  44. package/src/tsconfig/server.json +6 -0
  45. package/src/vite/async-utils.ts +61 -0
  46. package/src/vite/client/build.ts +223 -0
  47. package/src/vite/client/index.ts +1 -0
  48. package/src/vite/client/plugins/html-generator.ts +75 -0
  49. package/src/vite/client/plugins/index.ts +5 -0
  50. package/src/vite/client/plugins/locales-generator.ts +126 -0
  51. package/src/vite/client/plugins/minifier.ts +22 -0
  52. package/src/vite/client/plugins/node-definitions-inliner.ts +224 -0
  53. package/src/vite/client/plugins/static-copy.ts +43 -0
  54. package/src/vite/defaults.ts +77 -0
  55. package/src/vite/errors.ts +37 -0
  56. package/src/vite/index.ts +3 -0
  57. package/src/vite/logger.ts +94 -0
  58. package/src/vite/node-red-launcher.ts +344 -0
  59. package/src/vite/plugin.ts +61 -0
  60. package/src/vite/plugins/build.ts +73 -0
  61. package/src/vite/plugins/index.ts +2 -0
  62. package/src/vite/plugins/server.ts +267 -0
  63. package/src/vite/server/build.ts +124 -0
  64. package/src/vite/server/index.ts +1 -0
  65. package/src/vite/server/plugins/index.ts +3 -0
  66. package/src/vite/server/plugins/output-wrapper.ts +109 -0
  67. package/src/vite/server/plugins/package-json-generator.ts +203 -0
  68. package/src/vite/server/plugins/type-generator.ts +285 -0
  69. package/src/vite/types.ts +369 -0
  70. package/src/vite/utils.ts +103 -0
@@ -0,0 +1,283 @@
1
+ <template>
2
+ <div ref="container" class="container">
3
+ <button
4
+ ref="expand-button"
5
+ class="red-ui-button red-ui-button-small expand-button"
6
+ @click="onClickExpand"
7
+ >
8
+ <i class="fa fa-expand"></i>
9
+ </button>
10
+ <div :id="editorId" ref="editor"></div>
11
+ <div v-show="error" class="node-red-vue-input-error-message">
12
+ {{ error }}
13
+ </div>
14
+ </div>
15
+ </template>
16
+
17
+ <script lang="ts">
18
+ // TODO: expose editor apis
19
+ import { defineComponent } from "vue";
20
+ export default defineComponent({
21
+ props: {
22
+ value: {
23
+ type: String,
24
+ default: "",
25
+ },
26
+ language: {
27
+ type: String,
28
+ default: "json",
29
+ validator: function (value) {
30
+ const allowedLanguages = [
31
+ "abap",
32
+ "apex",
33
+ "azcli",
34
+ "bat",
35
+ "bicep",
36
+ "cameligo",
37
+ "clojure",
38
+ "coffee",
39
+ "cpp",
40
+ "csharp",
41
+ "csp",
42
+ "css",
43
+ "cypher",
44
+ "dart",
45
+ "dockerfile",
46
+ "ecl",
47
+ "elixir",
48
+ "flow9",
49
+ "freemarker2",
50
+ "fsharp",
51
+ "go",
52
+ "graphql",
53
+ "handlebars",
54
+ "hcl",
55
+ "html",
56
+ "ini",
57
+ "java",
58
+ "javascript",
59
+ "json",
60
+ "julia",
61
+ "kotlin",
62
+ "less",
63
+ "lexon",
64
+ "liquid",
65
+ "lua",
66
+ "m3",
67
+ "markdown",
68
+ "mdx",
69
+ "mips",
70
+ "msdax",
71
+ "mysql",
72
+ "objective-c",
73
+ "pascal",
74
+ "pascaligo",
75
+ "perl",
76
+ "pgsql",
77
+ "php",
78
+ "pla",
79
+ "postiats",
80
+ "powerquery",
81
+ "powershell",
82
+ "protobuf",
83
+ "pub",
84
+ "python",
85
+ "qsharp",
86
+ "r",
87
+ "razor",
88
+ "redis",
89
+ "redshift",
90
+ "restructuredtext",
91
+ "ruby",
92
+ "rust",
93
+ "sb",
94
+ "scala",
95
+ "scheme",
96
+ "scss",
97
+ "shell",
98
+ "solidity",
99
+ "sophia",
100
+ "sparql",
101
+ "sql",
102
+ "st",
103
+ "swift",
104
+ "systemverilog",
105
+ "tcl",
106
+ "twig",
107
+ "typescript",
108
+ "typespec",
109
+ "vb",
110
+ "wgsl",
111
+ "xml",
112
+ "yaml",
113
+ ];
114
+ const isValid = allowedLanguages.includes(value);
115
+ if (!isValid) {
116
+ console.warn(
117
+ `[WARN]: Invalid value for 'type' property: "${value}". ` +
118
+ `Expected one of: ${allowedLanguages.join(", ")}`,
119
+ );
120
+ }
121
+ return isValid;
122
+ },
123
+ },
124
+ error: {
125
+ type: String,
126
+ default: "",
127
+ },
128
+ },
129
+ emits: ["update:value"],
130
+ editor: null,
131
+ data() {
132
+ const stateId = Math.random().toString(36).substring(2, 9);
133
+ return {
134
+ editorId: "node-red-editor-" + stateId,
135
+ stateId,
136
+ };
137
+ },
138
+ mounted() {
139
+ // NOTE: jquery wrapper is used because RED.popover.tooltip needs it
140
+ const expandButton = $(this.$refs["expand-button"]);
141
+ RED.popover.tooltip(expandButton, RED._("node-red:common.label.expand"));
142
+ this.mountEditor();
143
+ this.createExpandeEditorTray();
144
+ },
145
+ beforeUnmount() {
146
+ if (this.editorInstance) {
147
+ try {
148
+ this.editorInstance.destroy();
149
+ } catch (err) {
150
+ console.error(`Error destroying editor for ID ${this.editorId}:`, err);
151
+ }
152
+ this.editorInstance = null;
153
+ }
154
+ },
155
+ methods: {
156
+ mountEditor() {
157
+ this.$nextTick(() => {
158
+ const containerEl = this.$refs.container;
159
+ const editorEl = this.$refs.editor;
160
+
161
+ if (containerEl && editorEl) {
162
+ try {
163
+ const inlineHeight = containerEl.style.height;
164
+ const inlineWidth = containerEl.style.width;
165
+ if (inlineHeight) {
166
+ editorEl.style.height = inlineHeight;
167
+ } else {
168
+ editorEl.style.height = "200px";
169
+ }
170
+
171
+ if (inlineWidth) {
172
+ editorEl.style.width = inlineWidth;
173
+ } else {
174
+ editorEl.style.width = "100%";
175
+ }
176
+
177
+ this.createEditorInstance();
178
+ } catch (e) {
179
+ console.error(
180
+ "[NodeRedEditorInput] Error setting initial editor style:",
181
+ e,
182
+ );
183
+ this.createEditorInstance();
184
+ }
185
+ } else {
186
+ console.error(
187
+ "[NodeRedEditorInput] Container or Editor div refs not found on mount.",
188
+ );
189
+ }
190
+ });
191
+ },
192
+ createEditorInstance() {
193
+ this.editorInstance = RED.editor.createEditor({
194
+ id: this.editorId,
195
+ mode: this.language,
196
+ value: this.value,
197
+ });
198
+ this.editorInstance.getSession().on("change", () => {
199
+ const currentValue = this.editorInstance.getValue();
200
+ if (currentValue !== this.value) {
201
+ this.$emit("update:value", currentValue);
202
+ }
203
+ });
204
+ },
205
+ createExpandeEditorTray() {
206
+ let expandedEditor;
207
+
208
+ const onCancel = () => {
209
+ setTimeout(() => {
210
+ this.editorInstance.focus();
211
+ }, 250);
212
+ RED.tray.close();
213
+ };
214
+
215
+ const onDone = () => {
216
+ expandedEditor.saveView();
217
+ this.editorInstance.setValue(expandedEditor.getValue(), -1);
218
+ setTimeout(() => {
219
+ this.editorInstance.restoreView();
220
+ this.editorInstance.focus();
221
+ }, 250);
222
+ RED.tray.close();
223
+ };
224
+
225
+ this.expandedEditorTray = {
226
+ title: "Editor",
227
+ focusElement: true,
228
+ width: "Infinity",
229
+ buttons: [
230
+ {
231
+ id: "node-dialog-cancel",
232
+ text: RED._("common.label.cancel"),
233
+ click: onCancel,
234
+ },
235
+ {
236
+ id: "node-dialog-ok",
237
+ text: RED._("common.label.done"),
238
+ class: "primary",
239
+ click: onDone,
240
+ },
241
+ ],
242
+ open: (tray) => {
243
+ const dialogForm = $(
244
+ '<form id="dialog-form" class="form-horizontal" autocomplete="off"></form>',
245
+ ).appendTo(tray.find(".red-ui-tray-body"));
246
+ dialogForm.html(
247
+ '<div id="expanded-editor-input" style="height: 100%"></div>',
248
+ );
249
+
250
+ expandedEditor = RED.editor.createEditor({
251
+ id: "expanded-editor-input",
252
+ stateId: this.stateId,
253
+ mode: this.language,
254
+ focus: true,
255
+ value: this.value,
256
+ });
257
+ dialogForm.i18n();
258
+ },
259
+ close: function () {
260
+ expandedEditor.destroy();
261
+ },
262
+ };
263
+ },
264
+ onClickExpand() {
265
+ RED.tray.show(this.expandedEditorTray);
266
+ },
267
+ },
268
+ });
269
+ </script>
270
+ <style scoped>
271
+ .container {
272
+ position: relative;
273
+ }
274
+
275
+ .expand-button {
276
+ position: absolute;
277
+ top: -23px;
278
+ right: 0px;
279
+ z-index: 10;
280
+ transition: color 0.3s ease;
281
+ cursor: pointer;
282
+ }
283
+ </style>
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <div style="display: flex; flex-direction: column; width: 100%">
3
+ <input
4
+ ref="inputField"
5
+ :type="type"
6
+ :value="internalValue"
7
+ :placeholder="placeholder"
8
+ style="width: 100%"
9
+ @input="onInput"
10
+ @focus="onFocus"
11
+ @blur="onBlur"
12
+ />
13
+ <div v-if="error" class="node-red-vue-input-error-message">
14
+ {{ error }}
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script lang="ts">
20
+ import { defineComponent } from "vue";
21
+
22
+ const SECRET_PATTERN = "*************";
23
+
24
+ export default defineComponent({
25
+ props: {
26
+ value: {
27
+ type: String,
28
+ default: "",
29
+ },
30
+ type: {
31
+ type: String,
32
+ default: "text",
33
+ },
34
+ placeholder: {
35
+ type: String,
36
+ default: "",
37
+ },
38
+ error: {
39
+ type: String,
40
+ default: "",
41
+ },
42
+ },
43
+ emits: ["update:value", "input"],
44
+ data() {
45
+ return {
46
+ internalValue: "",
47
+ };
48
+ },
49
+ beforeMount() {
50
+ this.internalValue = this.value;
51
+ this.onBlur();
52
+ },
53
+ methods: {
54
+ onInput(event) {
55
+ this.internalValue = event.target.value;
56
+ this.$emit("update:value", this.internalValue);
57
+ this.$emit("input", this.internalValue);
58
+ },
59
+ onFocus() {
60
+ if (this.type === "password" && this.internalValue === SECRET_PATTERN) {
61
+ this.internalValue = "";
62
+ }
63
+ },
64
+ onBlur() {
65
+ if (this.type === "password" && this.value === "__PWD__") {
66
+ this.internalValue = SECRET_PATTERN;
67
+ }
68
+ },
69
+ },
70
+ });
71
+ </script>
@@ -0,0 +1,369 @@
1
+ <template>
2
+ <div>
3
+ <div v-for="field in configFields" :key="field.key" class="form-row">
4
+ <span class="nrg-label">
5
+ {{ field.label }}
6
+ <span v-if="field.required" class="nrg-required">*</span>
7
+ </span>
8
+
9
+ <NodeRedInput
10
+ v-if="field.inputType === 'text' || field.inputType === 'number'"
11
+ :value="node[field.key]"
12
+ :type="field.htmlType"
13
+ :error="errors[`node.${field.key}`]"
14
+ @update:value="node[field.key] = $event"
15
+ />
16
+
17
+ <input
18
+ v-else-if="field.inputType === 'boolean'"
19
+ type="checkbox"
20
+ :checked="node[field.key]"
21
+ style="width: auto; margin: 0"
22
+ @change="
23
+ (e) => {
24
+ node[field.key] = (e.target as HTMLInputElement).checked;
25
+ }
26
+ "
27
+ />
28
+
29
+ <NodeRedSelectInput
30
+ v-else-if="field.inputType === 'select'"
31
+ :value="node[field.key]"
32
+ :options="field.options!"
33
+ :multiple="field.multiple"
34
+ :error="errors[`node.${field.key}`]"
35
+ @update:value="node[field.key] = $event"
36
+ />
37
+
38
+ <NodeRedTypedInput
39
+ v-else-if="field.inputType === 'typed'"
40
+ :value="node[field.key]"
41
+ :types="field.types"
42
+ :error="errors[`node.${field.key}`]"
43
+ @update:value="node[field.key] = $event"
44
+ />
45
+
46
+ <NodeRedConfigInput
47
+ v-else-if="field.inputType === 'config'"
48
+ :value="node[field.key]"
49
+ :type="field.configType!"
50
+ :node="node"
51
+ :prop-name="field.key"
52
+ :error="errors[`node.${field.key}`]"
53
+ @update:value="node[field.key] = $event"
54
+ />
55
+
56
+ <div v-else-if="field.inputType === 'array-text'">
57
+ <span
58
+ style="
59
+ display: block;
60
+ font-size: 11px;
61
+ color: var(--red-ui-text-color-disabled, #999);
62
+ margin-bottom: 4px;
63
+ "
64
+ >
65
+ One entry per line
66
+ </span>
67
+ <textarea
68
+ :value="
69
+ Array.isArray(node[field.key])
70
+ ? node[field.key].join('\n')
71
+ : (node[field.key] ?? '')
72
+ "
73
+ rows="4"
74
+ style="
75
+ width: 100%;
76
+ resize: vertical;
77
+ font-family: monospace;
78
+ font-size: 13px;
79
+ "
80
+ @input="
81
+ node[field.key] = ($event.target as HTMLTextAreaElement).value
82
+ .split('\n')
83
+ .filter(Boolean)
84
+ "
85
+ />
86
+ <span
87
+ v-if="errors[`node.${field.key}`]"
88
+ class="node-red-vue-input-error-message"
89
+ >
90
+ {{ errors[`node.${field.key}`] }}
91
+ </span>
92
+ </div>
93
+
94
+ <NodeRedEditorInput
95
+ v-else-if="field.inputType === 'editor'"
96
+ :value="node[field.key]"
97
+ :language="field.language"
98
+ :error="errors[`node.${field.key}`]"
99
+ @update:value="node[field.key] = $event"
100
+ />
101
+ </div>
102
+
103
+ <div
104
+ v-for="field in credentialFields"
105
+ :key="`cred-${field.key}`"
106
+ class="form-row"
107
+ >
108
+ <span class="nrg-label">
109
+ {{ field.label }}
110
+ <span v-if="field.required" class="nrg-required">*</span>
111
+ </span>
112
+
113
+ <NodeRedInput
114
+ :value="node.credentials[field.key]"
115
+ :type="field.htmlType"
116
+ :error="errors[`node.credentials.${field.key}`]"
117
+ @update:value="node.credentials[field.key] = $event"
118
+ />
119
+ </div>
120
+ </div>
121
+ </template>
122
+
123
+ <script lang="ts">
124
+ import type { PropType } from "vue";
125
+ import { defineComponent } from "vue";
126
+ import NodeRedInput from "./node-red-input.vue";
127
+ import NodeRedSelectInput from "./node-red-select-input.vue";
128
+ import NodeRedTypedInput from "./node-red-typed-input.vue";
129
+ import NodeRedConfigInput from "./node-red-config-input.vue";
130
+ import NodeRedEditorInput from "./node-red-editor-input.vue";
131
+
132
+ // System fields managed by Node-RED — not shown in the editor form.
133
+ const SKIP_FIELDS = new Set([
134
+ "id",
135
+ "type",
136
+ "x",
137
+ "y",
138
+ "z",
139
+ "g",
140
+ "wires",
141
+ "credentials",
142
+ "_users",
143
+ "validateInput",
144
+ "validateOutput",
145
+ ]);
146
+
147
+ interface FieldSchema {
148
+ type?: string | string[];
149
+ properties?: Record<string, FieldSchema>;
150
+ required?: string[];
151
+ enum?: any[];
152
+ format?: string;
153
+ title?: string;
154
+ description?: string;
155
+ default?: any;
156
+ items?: FieldSchema;
157
+ "node-type"?: string;
158
+ "x-typed-types"?: string[];
159
+ "x-editor-language"?: string;
160
+ [key: string]: any;
161
+ }
162
+
163
+ interface FormField {
164
+ key: string;
165
+ label: string;
166
+ inputType:
167
+ | "text"
168
+ | "number"
169
+ | "boolean"
170
+ | "select"
171
+ | "typed"
172
+ | "config"
173
+ | "editor";
174
+ required: boolean;
175
+ htmlType?: "text" | "number" | "password";
176
+ options?: Array<{ value: string; label: string }>;
177
+ multiple?: boolean;
178
+ types?: string[];
179
+ configType?: string;
180
+ language?: string;
181
+ }
182
+
183
+ function formatLabel(key: string): string {
184
+ return key
185
+ .replace(/([A-Z])/g, " $1")
186
+ .replace(/^./, (s) => s.toUpperCase())
187
+ .trim();
188
+ }
189
+
190
+ function isTypedInput(schema: FieldSchema): boolean {
191
+ return (
192
+ schema.type === "object" &&
193
+ !!schema.properties?.value &&
194
+ !!schema.properties?.type
195
+ );
196
+ }
197
+
198
+ function buildField(
199
+ key: string,
200
+ schema: FieldSchema,
201
+ required: boolean,
202
+ ): FormField {
203
+ const label = schema.title || formatLabel(key);
204
+
205
+ // NodeRef → config input
206
+ if (schema["node-type"]) {
207
+ return {
208
+ key,
209
+ label,
210
+ inputType: "config",
211
+ required,
212
+ configType: schema["node-type"],
213
+ };
214
+ }
215
+
216
+ // TypedInput → typed input widget
217
+ if (isTypedInput(schema)) {
218
+ return {
219
+ key,
220
+ label,
221
+ inputType: "typed",
222
+ required,
223
+ types: schema["x-typed-types"],
224
+ };
225
+ }
226
+
227
+ // Array with enum items → multi-select
228
+ if (schema.type === "array" && schema.items?.enum) {
229
+ return {
230
+ key,
231
+ label,
232
+ inputType: "select",
233
+ required,
234
+ multiple: true,
235
+ options: schema.items.enum.map((v: any) => ({
236
+ value: String(v),
237
+ label: String(v),
238
+ })),
239
+ };
240
+ }
241
+
242
+ // Top-level enum → single select
243
+ if (schema.enum) {
244
+ return {
245
+ key,
246
+ label,
247
+ inputType: "select",
248
+ required,
249
+ multiple: false,
250
+ options: schema.enum.map((v: any) => ({
251
+ value: String(v),
252
+ label: String(v),
253
+ })),
254
+ };
255
+ }
256
+
257
+ const rawType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
258
+
259
+ switch (rawType) {
260
+ case "boolean":
261
+ return { key, label, inputType: "boolean", required };
262
+
263
+ case "number":
264
+ case "integer":
265
+ return { key, label, inputType: "number", required, htmlType: "number" };
266
+
267
+ case "array":
268
+ if (schema["x-editor-language"]) {
269
+ return {
270
+ key,
271
+ label,
272
+ inputType: "editor",
273
+ required,
274
+ language: schema["x-editor-language"],
275
+ };
276
+ }
277
+ // Plain array of strings → comma-separated text input
278
+ return { key, label, inputType: "array-text", required };
279
+
280
+ case "object":
281
+ if (schema["x-editor-language"]) {
282
+ return {
283
+ key,
284
+ label,
285
+ inputType: "editor",
286
+ required,
287
+ language: schema["x-editor-language"],
288
+ };
289
+ }
290
+ // Plain object → text input (stored as JSON string)
291
+ return { key, label, inputType: "text", required, htmlType: "text" };
292
+
293
+ default:
294
+ // string (or untyped)
295
+ return {
296
+ key,
297
+ label,
298
+ inputType: "text",
299
+ required,
300
+ htmlType: schema.format === "password" ? "password" : "text",
301
+ };
302
+ }
303
+ }
304
+
305
+ export default defineComponent({
306
+ name: "NodeRedJsonSchemaForm",
307
+ components: {
308
+ NodeRedInput,
309
+ NodeRedSelectInput,
310
+ NodeRedTypedInput,
311
+ NodeRedConfigInput,
312
+ NodeRedEditorInput,
313
+ },
314
+ props: {
315
+ node: {
316
+ type: Object as PropType<Record<string, any>>,
317
+ required: true,
318
+ },
319
+ schema: {
320
+ type: Object as PropType<FieldSchema>,
321
+ required: true,
322
+ },
323
+ errors: {
324
+ type: Object as PropType<Record<string, string>>,
325
+ default: () => ({}),
326
+ },
327
+ },
328
+ computed: {
329
+ configFields(): FormField[] {
330
+ if (!this.schema?.properties) return [];
331
+ const required = new Set(this.schema.required ?? []);
332
+ return Object.entries(this.schema.properties)
333
+ .filter(([key]) => !SKIP_FIELDS.has(key))
334
+ .map(([key, propSchema]) =>
335
+ buildField(key, propSchema as FieldSchema, required.has(key)),
336
+ );
337
+ },
338
+ credentialFields(): FormField[] {
339
+ const credSchema = this.schema?.properties?.credentials as
340
+ | FieldSchema
341
+ | undefined;
342
+ if (!credSchema?.properties) return [];
343
+ const required = new Set(credSchema.required ?? []);
344
+ return Object.entries(credSchema.properties).map(([key, propSchema]) => {
345
+ const f = buildField(key, propSchema as FieldSchema, required.has(key));
346
+ // Force credential fields to be text/password inputs
347
+ if (f.inputType !== "text") {
348
+ return {
349
+ ...f,
350
+ inputType: "text" as const,
351
+ htmlType:
352
+ (propSchema as FieldSchema).format === "password"
353
+ ? ("password" as const)
354
+ : ("text" as const),
355
+ };
356
+ }
357
+ return f;
358
+ });
359
+ },
360
+ },
361
+ });
362
+ </script>
363
+
364
+ <style scoped>
365
+ .nrg-required {
366
+ color: var(--red-ui-text-color-error);
367
+ margin-left: 2px;
368
+ }
369
+ </style>