ciyidogan commited on
Commit
30b54a5
·
verified ·
1 Parent(s): b6b8282

Upload json-editor.component.ts

Browse files
flare-ui/src/app/shared/json-editor/json-editor.component.ts ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, Input, forwardRef, OnInit, ViewChild, ElementRef } from '@angular/core';
2
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';
3
+ import { CommonModule } from '@angular/common';
4
+ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
5
+ import { MatFormFieldModule } from '@angular/material/form-field';
6
+ import { MatInputModule } from '@angular/material/input';
7
+ import { MatIconModule } from '@angular/material/icon';
8
+ import { MatChipsModule } from '@angular/material/chips';
9
+ import { MatExpansionModule } from '@angular/material/expansion';
10
+
11
+ @Component({
12
+ selector: 'app-json-editor',
13
+ standalone: true,
14
+ imports: [
15
+ CommonModule,
16
+ FormsModule,
17
+ ReactiveFormsModule,
18
+ MatFormFieldModule,
19
+ MatInputModule,
20
+ MatIconModule,
21
+ MatChipsModule,
22
+ MatExpansionModule
23
+ ],
24
+ providers: [
25
+ {
26
+ provide: NG_VALUE_ACCESSOR,
27
+ useExisting: forwardRef(() => JsonEditorComponent),
28
+ multi: true
29
+ }
30
+ ],
31
+ template: `
32
+ <mat-form-field appearance="outline"
33
+ class="full-width"
34
+ [class.json-valid]="isValidJson()"
35
+ [class.json-invalid]="!isValidJson() && value">
36
+ <mat-label>{{ label }}</mat-label>
37
+ <textarea matInput
38
+ #textareaRef
39
+ [(ngModel)]="value"
40
+ [rows]="rows"
41
+ [placeholder]="placeholder"
42
+ (keydown)="handleKeydown($event)"
43
+ (click)="handleCursorMove($event)"
44
+ (keyup)="handleCursorMove($event)"
45
+ (blur)="onTouched()"
46
+ class="code-editor"></textarea>
47
+ <mat-hint>{{ hint }}</mat-hint>
48
+ @if (!isValidJson() && value) {
49
+ <mat-error>Invalid JSON format</mat-error>
50
+ }
51
+ </mat-form-field>
52
+
53
+ <!-- JSON Validation Indicator -->
54
+ <div class="json-validation-status">
55
+ @if (isValidJson()) {
56
+ <mat-icon class="valid">check_circle</mat-icon>
57
+ <span class="valid">Valid JSON</span>
58
+ } @else if (value) {
59
+ <mat-icon class="invalid">error</mat-icon>
60
+ <span class="invalid">Invalid JSON</span>
61
+ }
62
+ </div>
63
+
64
+ <!-- Collapsible Variables Panel -->
65
+ @if (availableVariables && availableVariables.length > 0) {
66
+ <mat-expansion-panel class="variables-panel">
67
+ <mat-expansion-panel-header>
68
+ <mat-panel-title>
69
+ <mat-icon>code</mat-icon>
70
+ Available Variables
71
+ </mat-panel-title>
72
+ <mat-panel-description>
73
+ Click to insert template variables
74
+ </mat-panel-description>
75
+ </mat-expansion-panel-header>
76
+
77
+ <mat-chip-set>
78
+ @for (variable of availableVariables; track variable) {
79
+ <mat-chip (click)="insertVariable(variable)">
80
+ {{ variable }}
81
+ </mat-chip>
82
+ }
83
+ </mat-chip-set>
84
+ </mat-expansion-panel>
85
+ }
86
+ `,
87
+ styles: [`
88
+ :host {
89
+ display: block;
90
+ margin-bottom: 16px;
91
+ }
92
+
93
+ .full-width {
94
+ width: 100%;
95
+ }
96
+
97
+ .code-editor {
98
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important;
99
+ font-size: 13px;
100
+ line-height: 1.5;
101
+ tab-size: 2;
102
+ -moz-tab-size: 2;
103
+ white-space: pre;
104
+ }
105
+
106
+ .json-validation-status {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 4px;
110
+ margin-top: -12px;
111
+ margin-bottom: 16px;
112
+ font-size: 12px;
113
+
114
+ mat-icon {
115
+ font-size: 16px;
116
+ width: 16px;
117
+ height: 16px;
118
+
119
+ &.valid {
120
+ color: #4caf50;
121
+ }
122
+
123
+ &.invalid {
124
+ color: #f44336;
125
+ }
126
+ }
127
+
128
+ span {
129
+ &.valid {
130
+ color: #4caf50;
131
+ }
132
+
133
+ &.invalid {
134
+ color: #f44336;
135
+ }
136
+ }
137
+ }
138
+
139
+ .variables-panel {
140
+ margin-bottom: 16px;
141
+ box-shadow: none;
142
+ border: 1px solid rgba(0, 0, 0, 0.12);
143
+
144
+ .mat-expansion-panel-header {
145
+ padding: 0 16px;
146
+ height: 40px;
147
+
148
+ .mat-panel-title {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 8px;
152
+ font-size: 13px;
153
+ color: #666;
154
+
155
+ mat-icon {
156
+ font-size: 18px;
157
+ width: 18px;
158
+ height: 18px;
159
+ }
160
+ }
161
+
162
+ .mat-panel-description {
163
+ font-size: 12px;
164
+ color: #999;
165
+ }
166
+ }
167
+
168
+ .mat-expansion-panel-body {
169
+ padding: 8px 16px 16px;
170
+ }
171
+
172
+ mat-chip-set {
173
+ mat-chip {
174
+ font-size: 12px;
175
+ min-height: 24px;
176
+ padding: 4px 8px;
177
+ cursor: pointer;
178
+ transition: all 0.2s;
179
+
180
+ &:hover {
181
+ background-color: #e3f2fd;
182
+ color: #1976d2;
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ // JSON field background colors
189
+ .json-valid {
190
+ textarea {
191
+ background-color: rgba(76, 175, 80, 0.05) !important;
192
+ }
193
+ }
194
+
195
+ .json-invalid {
196
+ textarea {
197
+ background-color: rgba(244, 67, 54, 0.05) !important;
198
+ }
199
+ }
200
+ `]
201
+ })
202
+ export class JsonEditorComponent implements ControlValueAccessor, OnInit {
203
+ @ViewChild('textareaRef') textareaRef!: ElementRef<HTMLTextAreaElement>;
204
+
205
+ @Input() label = 'JSON Editor';
206
+ @Input() placeholder = '{}';
207
+ @Input() hint = 'Enter valid JSON';
208
+ @Input() rows = 8;
209
+ @Input() availableVariables: string[] = [];
210
+ @Input() variableReplacer?: (json: string) => string;
211
+
212
+ value = '';
213
+ onChange: (value: string) => void = () => {};
214
+ onTouched: () => void = () => {};
215
+
216
+ private bracketPairs: { [key: string]: string } = {
217
+ '{': '}',
218
+ '[': ']',
219
+ '(': ')'
220
+ };
221
+
222
+ private cursorPosition = 0;
223
+
224
+ ngOnInit() {}
225
+
226
+ writeValue(value: string): void {
227
+ this.value = value || '';
228
+ }
229
+
230
+ registerOnChange(fn: (value: string) => void): void {
231
+ this.onChange = fn;
232
+ }
233
+
234
+ registerOnTouched(fn: () => void): void {
235
+ this.onTouched = fn;
236
+ }
237
+
238
+ setDisabledState?(isDisabled: boolean): void {
239
+ // Handle disabled state if needed
240
+ }
241
+
242
+ isValidJson(): boolean {
243
+ if (!this.value || !this.value.trim()) return true;
244
+
245
+ try {
246
+ let jsonStr = this.value;
247
+
248
+ // If variableReplacer is provided, use it to replace variables for validation
249
+ if (this.variableReplacer) {
250
+ jsonStr = this.variableReplacer(jsonStr);
251
+ } else {
252
+ // Default variable replacement for validation
253
+ jsonStr = this.replaceVariablesForValidation(jsonStr);
254
+ }
255
+
256
+ JSON.parse(jsonStr);
257
+ return true;
258
+ } catch {
259
+ return false;
260
+ }
261
+ }
262
+
263
+ private replaceVariablesForValidation(jsonStr: string): string {
264
+ let processed = jsonStr;
265
+
266
+ processed = processed.replace(/\{\{([^}]+)\}\}/g, (match, variablePath) => {
267
+ if (variablePath.includes('variables.')) {
268
+ const varName = variablePath.split('.').pop()?.toLowerCase() || '';
269
+
270
+ const numericVars = ['count', 'passenger_count', 'timeout_seconds', 'retry_count', 'amount', 'price', 'quantity', 'age', 'id'];
271
+ const booleanVars = ['enabled', 'published', 'is_active', 'confirmed', 'canceled', 'deleted', 'required'];
272
+
273
+ if (numericVars.some(v => varName.includes(v))) {
274
+ return '1';
275
+ } else if (booleanVars.some(v => varName.includes(v))) {
276
+ return 'true';
277
+ } else {
278
+ return '"placeholder"';
279
+ }
280
+ }
281
+
282
+ return '"placeholder"';
283
+ });
284
+
285
+ return processed;
286
+ }
287
+
288
+ handleKeydown(event: KeyboardEvent): void {
289
+ if (event.key === 'Tab') {
290
+ this.handleTabKey(event);
291
+ } else if (event.key === 'Enter') {
292
+ this.handleEnterKey(event);
293
+ } else if (event.key in this.bracketPairs || Object.values(this.bracketPairs).includes(event.key)) {
294
+ this.handleBracketInput(event);
295
+ }
296
+ }
297
+
298
+ handleTabKey(event: KeyboardEvent): void {
299
+ event.preventDefault();
300
+
301
+ const textarea = event.target as HTMLTextAreaElement;
302
+ const start = textarea.selectionStart;
303
+ const end = textarea.selectionEnd;
304
+
305
+ if (start !== end) {
306
+ this.handleBlockIndent(textarea, !event.shiftKey);
307
+ } else {
308
+ const newValue = this.value.substring(0, start) + '\t' + this.value.substring(end);
309
+ this.updateValue(newValue);
310
+
311
+ setTimeout(() => {
312
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
313
+ textarea.focus();
314
+ }, 0);
315
+ }
316
+ }
317
+
318
+ private handleBlockIndent(textarea: HTMLTextAreaElement, indent: boolean): void {
319
+ const start = textarea.selectionStart;
320
+ const end = textarea.selectionEnd;
321
+ const value = this.value;
322
+
323
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1;
324
+ const lineEnd = value.indexOf('\n', end);
325
+ const actualEnd = lineEnd === -1 ? value.length : lineEnd;
326
+
327
+ const selectedLines = value.substring(lineStart, actualEnd);
328
+ const lines = selectedLines.split('\n');
329
+
330
+ let newLines: string[];
331
+ if (indent) {
332
+ newLines = lines.map(line => '\t' + line);
333
+ } else {
334
+ newLines = lines.map(line => line.startsWith('\t') ? line.substring(1) : line);
335
+ }
336
+
337
+ const newText = newLines.join('\n');
338
+ const newValue = value.substring(0, lineStart) + newText + value.substring(actualEnd);
339
+
340
+ this.updateValue(newValue);
341
+
342
+ setTimeout(() => {
343
+ const lengthDiff = newText.length - selectedLines.length;
344
+ textarea.selectionStart = lineStart;
345
+ textarea.selectionEnd = actualEnd + lengthDiff;
346
+ textarea.focus();
347
+ }, 0);
348
+ }
349
+
350
+ handleEnterKey(event: KeyboardEvent): void {
351
+ event.preventDefault();
352
+
353
+ const textarea = event.target as HTMLTextAreaElement;
354
+ const start = textarea.selectionStart;
355
+ const value = this.value;
356
+
357
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1;
358
+ const currentLine = value.substring(lineStart, start);
359
+ const indent = currentLine.match(/^[\t ]*/)?.[0] || '';
360
+
361
+ const prevChar = value[start - 1];
362
+ const nextChar = value[start];
363
+
364
+ let newLineContent = '\n' + indent;
365
+ let cursorOffset = newLineContent.length;
366
+
367
+ if (prevChar in this.bracketPairs) {
368
+ newLineContent = '\n' + indent + '\t';
369
+ cursorOffset = newLineContent.length;
370
+
371
+ if (nextChar === this.bracketPairs[prevChar]) {
372
+ newLineContent += '\n' + indent;
373
+ }
374
+ }
375
+
376
+ const newValue = value.substring(0, start) + newLineContent + value.substring(start);
377
+ this.updateValue(newValue);
378
+
379
+ setTimeout(() => {
380
+ textarea.selectionStart = textarea.selectionEnd = start + cursorOffset;
381
+ textarea.focus();
382
+ }, 0);
383
+ }
384
+
385
+ handleBracketInput(event: KeyboardEvent): void {
386
+ const textarea = event.target as HTMLTextAreaElement;
387
+ const char = event.key;
388
+
389
+ if (char in this.bracketPairs) {
390
+ event.preventDefault();
391
+
392
+ const start = textarea.selectionStart;
393
+ const end = textarea.selectionEnd;
394
+ const value = this.value;
395
+
396
+ const selectedText = value.substring(start, end);
397
+ const closingBracket = this.bracketPairs[char];
398
+
399
+ let newValue: string;
400
+ let cursorPos: number;
401
+
402
+ if (selectedText) {
403
+ newValue = value.substring(0, start) + char + selectedText + closingBracket + value.substring(end);
404
+ cursorPos = start + 1 + selectedText.length;
405
+ } else {
406
+ newValue = value.substring(0, start) + char + closingBracket + value.substring(end);
407
+ cursorPos = start + 1;
408
+ }
409
+
410
+ this.updateValue(newValue);
411
+
412
+ setTimeout(() => {
413
+ textarea.selectionStart = textarea.selectionEnd = cursorPos;
414
+ textarea.focus();
415
+ }, 0);
416
+ } else if (Object.values(this.bracketPairs).includes(char)) {
417
+ const start = textarea.selectionStart;
418
+ const value = this.value;
419
+ const nextChar = value[start];
420
+
421
+ if (nextChar === char) {
422
+ event.preventDefault();
423
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
424
+ }
425
+ }
426
+ }
427
+
428
+ handleCursorMove(event: any): void {
429
+ this.cursorPosition = event.target.selectionStart;
430
+ }
431
+
432
+ insertVariable(variable: string): void {
433
+ const textarea = this.textareaRef.nativeElement;
434
+ const start = this.cursorPosition;
435
+ const variableText = `{{${variable}}}`;
436
+
437
+ const newValue = this.value.substring(0, start) + variableText + this.value.substring(start);
438
+ this.updateValue(newValue);
439
+
440
+ setTimeout(() => {
441
+ const newPos = start + variableText.length;
442
+ textarea.selectionStart = textarea.selectionEnd = newPos;
443
+ textarea.focus();
444
+ }, 0);
445
+ }
446
+
447
+ private updateValue(newValue: string): void {
448
+ this.value = newValue;
449
+ this.onChange(newValue);
450
+ }
451
+ }