ciyidogan commited on
Commit
6c88c4d
·
verified ·
1 Parent(s): 07ed1a3

Update flare-ui/src/app/dialogs/api-edit-dialog/api-edit-dialog.component.ts

Browse files
flare-ui/src/app/dialogs/api-edit-dialog/api-edit-dialog.component.ts CHANGED
@@ -1,631 +1,644 @@
1
- import { Component, Inject, OnInit } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
- import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormArray } from '@angular/forms';
4
- import { FormsModule } from '@angular/forms';
5
- import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
6
- import { MatTabsModule } from '@angular/material/tabs';
7
- import { MatFormFieldModule } from '@angular/material/form-field';
8
- import { MatInputModule } from '@angular/material/input';
9
- import { MatSelectModule } from '@angular/material/select';
10
- import { MatCheckboxModule } from '@angular/material/checkbox';
11
- import { MatButtonModule } from '@angular/material/button';
12
- import { MatIconModule } from '@angular/material/icon';
13
- import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
14
- import { MatDividerModule } from '@angular/material/divider';
15
- import { MatExpansionModule } from '@angular/material/expansion';
16
- import { MatChipsModule } from '@angular/material/chips';
17
- import { ApiService } from '../../services/api.service';
18
- import { MatMenuModule } from '@angular/material/menu';
19
-
20
- @Component({
21
- selector: 'app-api-edit-dialog',
22
- standalone: true,
23
- imports: [
24
- CommonModule,
25
- ReactiveFormsModule,
26
- FormsModule,
27
- MatDialogModule,
28
- MatTabsModule,
29
- MatFormFieldModule,
30
- MatInputModule,
31
- MatSelectModule,
32
- MatCheckboxModule,
33
- MatButtonModule,
34
- MatIconModule,
35
- MatSnackBarModule,
36
- MatDividerModule,
37
- MatExpansionModule,
38
- MatChipsModule,
39
- MatMenuModule // ✅ MatMenuModule eklendi
40
- ],
41
- templateUrl: './api-edit-dialog.component.html',
42
- styleUrls: ['./api-edit-dialog.component.scss']
43
- })
44
- export default class ApiEditDialogComponent implements OnInit {
45
- form!: FormGroup;
46
- saving = false;
47
- testing = false;
48
- testResult: any = null;
49
- testRequestJson = '{}';
50
- allIntentParameters: string[] = [];
51
- responseMappingVariables: string[] = [];
52
- activeTabIndex = 0;
53
-
54
- httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
55
- retryStrategies = ['static', 'exponential'];
56
- variableTypes = ['str', 'int', 'float', 'bool', 'date'];
57
-
58
- private cursorPositions: { [key: string]: number } = {};
59
-
60
- constructor(
61
- private fb: FormBuilder,
62
- private apiService: ApiService,
63
- private snackBar: MatSnackBar,
64
- public dialogRef: MatDialogRef<ApiEditDialogComponent>,
65
- @Inject(MAT_DIALOG_DATA) public data: any
66
- ) {}
67
-
68
- ngOnInit() {
69
- this.initializeForm();
70
- this.loadIntentParameters();
71
-
72
- // Aktif tab'ı ayarla
73
- if (this.data.activeTab !== undefined) {
74
- this.activeTabIndex = this.data.activeTab;
75
- }
76
-
77
- if ((this.data.mode === 'edit' || this.data.mode === 'test') && this.data.api) {
78
- this.populateForm(this.data.api);
79
- } else if (this.data.mode === 'duplicate' && this.data.api) {
80
- const duplicateData = { ...this.data.api };
81
- duplicateData.name = duplicateData.name + '_copy';
82
- delete duplicateData.last_update_date;
83
- this.populateForm(duplicateData);
84
- }
85
-
86
- // Test modunda açıldıysa test JSON'ını hazırla
87
- if (this.data.mode === 'test') {
88
- setTimeout(() => {
89
- this.updateTestRequestJson();
90
- }, 100);
91
- }
92
-
93
- // Watch response mappings changes
94
- this.form.get('response_mappings')?.valueChanges.subscribe(() => {
95
- this.updateResponseMappingVariables();
96
- });
97
- }
98
-
99
- initializeForm() {
100
- this.form = this.fb.group({
101
- // General Tab
102
- name: ['', [Validators.required, Validators.pattern(/^[a-zA-Z0-9_]+$/)]],
103
- url: ['', [Validators.required, Validators.pattern(/^https?:\/\/.+/)]],
104
- method: ['POST', Validators.required],
105
- body_template: ['{}'],
106
- timeout_seconds: [10, [Validators.required, Validators.min(1), Validators.max(300)]],
107
- response_prompt: [''],
108
- response_mappings: this.fb.array([]),
109
-
110
- // Headers Tab
111
- headers: this.fb.array([]),
112
-
113
- // Retry Settings
114
- retry: this.fb.group({
115
- retry_count: [3, [Validators.required, Validators.min(0), Validators.max(10)]],
116
- backoff_seconds: [2, [Validators.required, Validators.min(1), Validators.max(60)]],
117
- strategy: ['static', Validators.required]
118
- }),
119
-
120
- // Auth Tab
121
- auth: this.fb.group({
122
- enabled: [false],
123
- token_endpoint: [''],
124
- response_token_path: ['token'],
125
- token_request_body: ['{}'],
126
- token_refresh_endpoint: [''],
127
- token_refresh_body: ['{}']
128
- }),
129
-
130
- // Proxy (optional)
131
- proxy: [''],
132
-
133
- // For race condition handling
134
- last_update_date: ['']
135
- });
136
-
137
- // Watch for auth enabled changes
138
- this.form.get('auth.enabled')?.valueChanges.subscribe(enabled => {
139
- const authGroup = this.form.get('auth');
140
- if (enabled) {
141
- authGroup?.get('token_endpoint')?.setValidators([Validators.required]);
142
- authGroup?.get('response_token_path')?.setValidators([Validators.required]);
143
- } else {
144
- authGroup?.get('token_endpoint')?.clearValidators();
145
- authGroup?.get('response_token_path')?.clearValidators();
146
- }
147
- authGroup?.get('token_endpoint')?.updateValueAndValidity();
148
- authGroup?.get('response_token_path')?.updateValueAndValidity();
149
- });
150
- }
151
-
152
- populateForm(api: any) {
153
- console.log('Populating form with API:', api);
154
-
155
- // Convert headers object to FormArray
156
- const headersArray = this.form.get('headers') as FormArray;
157
- headersArray.clear();
158
-
159
- if (api.headers) {
160
- if (Array.isArray(api.headers)) {
161
- api.headers.forEach((header: any) => {
162
- headersArray.push(this.createHeaderFormGroup(header.key || '', header.value || ''));
163
- });
164
- } else if (typeof api.headers === 'object') {
165
- Object.entries(api.headers).forEach(([key, value]) => {
166
- headersArray.push(this.createHeaderFormGroup(key, value as string));
167
- });
168
- }
169
- }
170
-
171
- // Convert response_mappings to FormArray
172
- const responseMappingsArray = this.form.get('response_mappings') as FormArray;
173
- responseMappingsArray.clear();
174
-
175
- if (api.response_mappings && Array.isArray(api.response_mappings)) {
176
- api.response_mappings.forEach((mapping: any) => {
177
- responseMappingsArray.push(this.createResponseMappingFormGroup(mapping));
178
- });
179
- }
180
-
181
- // Convert body_template to JSON string if it's an object
182
- if (api.body_template && typeof api.body_template === 'object') {
183
- api.body_template = JSON.stringify(api.body_template, null, 2);
184
- }
185
-
186
- // Convert auth bodies to JSON strings
187
- if (api.auth) {
188
- if (api.auth.token_request_body && typeof api.auth.token_request_body === 'object') {
189
- api.auth.token_request_body = JSON.stringify(api.auth.token_request_body, null, 2);
190
- }
191
- if (api.auth.token_refresh_body && typeof api.auth.token_refresh_body === 'object') {
192
- api.auth.token_refresh_body = JSON.stringify(api.auth.token_refresh_body, null, 2);
193
- }
194
- }
195
-
196
- const formData = { ...api };
197
-
198
- // headers array'ini kaldır çünkü zaten FormArray'e ekledik
199
- delete formData.headers;
200
- delete formData.response_mappings;
201
-
202
- // Patch form values
203
- this.form.patchValue(formData);
204
-
205
- // Disable name field if editing or testing
206
- if (this.data.mode === 'edit' || this.data.mode === 'test') {
207
- this.form.get('name')?.disable();
208
- }
209
- }
210
-
211
- get headers() {
212
- return this.form.get('headers') as FormArray;
213
- }
214
-
215
- get responseMappings() {
216
- return this.form.get('response_mappings') as FormArray;
217
- }
218
-
219
- createHeaderFormGroup(key = '', value = ''): FormGroup {
220
- return this.fb.group({
221
- key: [key, Validators.required],
222
- value: [value, Validators.required]
223
- });
224
- }
225
-
226
- createResponseMappingFormGroup(data: any = {}): FormGroup {
227
- return this.fb.group({
228
- variable_name: [data.variable_name || '', [Validators.required, Validators.pattern(/^[a-z_][a-z0-9_]*$/)]],
229
- type: [data.type || 'str', Validators.required],
230
- json_path: [data.json_path || '', Validators.required],
231
- caption: [data.caption || '', Validators.required]
232
- });
233
- }
234
-
235
- addHeader() {
236
- this.headers.push(this.createHeaderFormGroup());
237
- }
238
-
239
- removeHeader(index: number) {
240
- this.headers.removeAt(index);
241
- }
242
-
243
- addResponseMapping() {
244
- this.responseMappings.push(this.createResponseMappingFormGroup());
245
- }
246
-
247
- removeResponseMapping(index: number) {
248
- this.responseMappings.removeAt(index);
249
- }
250
-
251
- insertHeaderValue(index: number, variable: string) {
252
- const headerGroup = this.headers.at(index);
253
- if (headerGroup) {
254
- const valueControl = headerGroup.get('value');
255
- if (valueControl) {
256
- const currentValue = valueControl.value || '';
257
- const newValue = currentValue + `{{${variable}}}`;
258
- valueControl.setValue(newValue);
259
- }
260
- }
261
- }
262
-
263
- getTemplateVariables(includeResponseMappings = true): string[] {
264
- const variables = new Set<string>();
265
-
266
- // Intent parameters
267
- this.allIntentParameters.forEach(param => {
268
- variables.add(`variables.${param}`);
269
- });
270
-
271
- // Auth tokens
272
- const apiName = this.form.get('name')?.value || 'api_name';
273
- variables.add(`auth_tokens.${apiName}.token`);
274
-
275
- // Response mappings
276
- if (includeResponseMappings) {
277
- this.responseMappingVariables.forEach(varName => {
278
- variables.add(`variables.${varName}`);
279
- });
280
- }
281
-
282
- // Config variables
283
- variables.add('config.work_mode');
284
- variables.add('config.cloud_token');
285
-
286
- return Array.from(variables).sort();
287
- }
288
-
289
- updateResponseMappingVariables() {
290
- this.responseMappingVariables = [];
291
- const mappings = this.responseMappings.value;
292
- mappings.forEach((mapping: any) => {
293
- if (mapping.variable_name) {
294
- this.responseMappingVariables.push(mapping.variable_name);
295
- }
296
- });
297
- }
298
-
299
- async loadIntentParameters() {
300
- try {
301
- const projects = await this.apiService.getProjects(false).toPromise();
302
- const params = new Set<string>();
303
-
304
- projects?.forEach(project => {
305
- project.versions?.forEach(version => {
306
- version.intents?.forEach(intent => {
307
- intent.parameters?.forEach((param: any) => {
308
- if (param.variable_name) {
309
- params.add(param.variable_name);
310
- }
311
- });
312
- });
313
- });
314
- });
315
-
316
- this.allIntentParameters = Array.from(params).sort();
317
- } catch (error) {
318
- console.error('Failed to load intent parameters:', error);
319
- }
320
- }
321
-
322
- validateJSON(field: string): boolean {
323
- const control = this.form.get(field);
324
- if (!control || !control.value) return true;
325
-
326
- try {
327
- const jsonStr = control.value;
328
- const processedJson = this.replaceVariablesForValidation(jsonStr);
329
- JSON.parse(processedJson);
330
- return true;
331
- } catch {
332
- return false;
333
- }
334
- }
335
-
336
- replaceVariablesForValidation(jsonStr: string): string {
337
- let processed = jsonStr;
338
-
339
- processed = processed.replace(/\{\{([^}]+)\}\}/g, (match, variablePath) => {
340
- if (variablePath.includes('variables.')) {
341
- const varName = variablePath.split('.').pop()?.toLowerCase() || '';
342
-
343
- const numericVars = ['count', 'passenger_count', 'timeout_seconds', 'retry_count', 'amount', 'price', 'quantity', 'age', 'id'];
344
- const booleanVars = ['enabled', 'published', 'is_active', 'confirmed', 'canceled', 'deleted', 'required'];
345
-
346
- if (numericVars.some(v => varName.includes(v))) {
347
- return '1';
348
- } else if (booleanVars.some(v => varName.includes(v))) {
349
- return 'true';
350
- } else {
351
- return '"placeholder"';
352
- }
353
- }
354
-
355
- return '"placeholder"';
356
- });
357
-
358
- return processed;
359
- }
360
-
361
- async testAPI() {
362
- const generalValid = this.form.get('url')?.valid && this.form.get('method')?.valid;
363
- if (!generalValid) {
364
- this.snackBar.open('Please fill in required fields first', 'Close', { duration: 3000 });
365
- return;
366
- }
367
-
368
- this.testing = true;
369
- this.testResult = null;
370
-
371
- try {
372
- const testData = this.prepareAPIData();
373
-
374
- let testRequestData = {};
375
- try {
376
- testRequestData = JSON.parse(this.testRequestJson);
377
- } catch (e) {
378
- this.snackBar.open('Invalid test request JSON', 'Close', {
379
- duration: 3000,
380
- panelClass: 'error-snackbar'
381
- });
382
- this.testing = false;
383
- return;
384
- }
385
-
386
- testData.test_request = testRequestData;
387
-
388
- const result = await this.apiService.testAPI(testData).toPromise();
389
- this.testResult = result;
390
-
391
- if (result.success) {
392
- this.snackBar.open(`API test successful! (${result.status_code})`, 'Close', {
393
- duration: 3000
394
- });
395
- } else {
396
- const errorMsg = result.error || `API returned status ${result.status_code}`;
397
- this.snackBar.open(`API test failed: ${errorMsg}`, 'Close', {
398
- duration: 5000,
399
- panelClass: 'error-snackbar'
400
- });
401
- }
402
- } catch (error: any) {
403
- this.testResult = {
404
- success: false,
405
- error: error.message || 'Test failed'
406
- };
407
- this.snackBar.open('API test failed', 'Close', {
408
- duration: 3000,
409
- panelClass: 'error-snackbar'
410
- });
411
- } finally {
412
- this.testing = false;
413
- }
414
- }
415
-
416
- prepareTestRequest(): any {
417
- try {
418
- const requestData = JSON.parse(this.testRequestJson);
419
- return requestData;
420
- } catch {
421
- return {};
422
- }
423
- }
424
-
425
- updateTestRequestJson() {
426
- const formValue = this.form.getRawValue();
427
- let bodyTemplate = {};
428
-
429
- try {
430
- bodyTemplate = JSON.parse(formValue.body_template);
431
- } catch {
432
- bodyTemplate = {};
433
- }
434
-
435
- const testData = this.replacePlaceholdersForTest(bodyTemplate);
436
- this.testRequestJson = JSON.stringify(testData, null, 2);
437
- }
438
-
439
- replacePlaceholdersForTest(obj: any): any {
440
- if (typeof obj === 'string') {
441
- let result = obj;
442
-
443
- result = result.replace(/\{\{variables\.origin\}\}/g, 'Istanbul');
444
- result = result.replace(/\{\{variables\.destination\}\}/g, 'Ankara');
445
- result = result.replace(/\{\{variables\.flight_date\}\}/g, '2025-06-15');
446
- result = result.replace(/\{\{variables\.passenger_count\}\}/g, '2');
447
- result = result.replace(/\{\{variables\.flight_number\}\}/g, 'TK123');
448
- result = result.replace(/\{\{variables\.pnr\}\}/g, 'ABC12');
449
- result = result.replace(/\{\{variables\.surname\}\}/g, 'Test');
450
-
451
- result = result.replace(/\{\{auth_tokens\.[^}]+\.token\}\}/g, 'test_token_123');
452
-
453
- result = result.replace(/\{\{config\.work_mode\}\}/g, 'hfcloud');
454
-
455
- result = result.replace(/\{\{[^}]+\}\}/g, 'test_value');
456
-
457
- return result;
458
- } else if (typeof obj === 'object' && obj !== null) {
459
- const result: any = Array.isArray(obj) ? [] : {};
460
- for (const key in obj) {
461
- result[key] = this.replacePlaceholdersForTest(obj[key]);
462
- }
463
- return result;
464
- }
465
- return obj;
466
- }
467
-
468
- prepareAPIData(): any {
469
- const formValue = this.form.getRawValue();
470
-
471
- const headers: any = {};
472
- formValue.headers.forEach((h: any) => {
473
- if (h.key && h.value) {
474
- headers[h.key] = h.value;
475
- }
476
- });
477
-
478
- let body_template = {};
479
- let auth_token_request_body = {};
480
- let auth_token_refresh_body = {};
481
-
482
- try {
483
- body_template = formValue.body_template ? JSON.parse(formValue.body_template) : {};
484
- } catch (e) {
485
- console.error('Invalid body_template JSON:', e);
486
- }
487
-
488
- try {
489
- auth_token_request_body = formValue.auth.token_request_body ? JSON.parse(formValue.auth.token_request_body) : {};
490
- } catch (e) {
491
- console.error('Invalid auth token_request_body JSON:', e);
492
- }
493
-
494
- try {
495
- auth_token_refresh_body = formValue.auth.token_refresh_body ? JSON.parse(formValue.auth.token_refresh_body) : {};
496
- } catch (e) {
497
- console.error('Invalid auth token_refresh_body JSON:', e);
498
- }
499
-
500
- const apiData: any = {
501
- name: formValue.name,
502
- url: formValue.url,
503
- method: formValue.method,
504
- headers,
505
- body_template,
506
- timeout_seconds: formValue.timeout_seconds,
507
- retry: formValue.retry,
508
- response_prompt: formValue.response_prompt,
509
- response_mappings: formValue.response_mappings || []
510
- };
511
-
512
- if (formValue.proxy) {
513
- apiData.proxy = formValue.proxy;
514
- }
515
-
516
- if (formValue.auth.enabled) {
517
- apiData.auth = {
518
- enabled: true,
519
- token_endpoint: formValue.auth.token_endpoint,
520
- response_token_path: formValue.auth.response_token_path,
521
- token_request_body: auth_token_request_body
522
- };
523
-
524
- if (formValue.auth.token_refresh_endpoint) {
525
- apiData.auth.token_refresh_endpoint = formValue.auth.token_refresh_endpoint;
526
- apiData.auth.token_refresh_body = auth_token_refresh_body;
527
- }
528
- }
529
-
530
- if (this.data.mode === 'edit' && formValue.last_update_date) {
531
- apiData.last_update_date = formValue.last_update_date;
532
- }
533
-
534
- return apiData;
535
- }
536
-
537
- async save() {
538
- if (this.data.mode === 'test') {
539
- this.cancel();
540
- return;
541
- }
542
-
543
- if (this.form.invalid) {
544
- Object.keys(this.form.controls).forEach(key => {
545
- this.form.get(key)?.markAsTouched();
546
- });
547
-
548
- const jsonFields = ['body_template', 'auth.token_request_body', 'auth.token_refresh_body'];
549
- for (const field of jsonFields) {
550
- if (!this.validateJSON(field)) {
551
- this.snackBar.open(`Invalid JSON in ${field}`, 'Close', {
552
- duration: 3000,
553
- panelClass: 'error-snackbar'
554
- });
555
- return;
556
- }
557
- }
558
-
559
- this.snackBar.open('Please fix validation errors', 'Close', { duration: 3000 });
560
- return;
561
- }
562
-
563
- this.saving = true;
564
- try {
565
- const apiData = this.prepareAPIData();
566
-
567
- if (this.data.mode === 'create' || this.data.mode === 'duplicate') {
568
- await this.apiService.createAPI(apiData).toPromise();
569
- this.snackBar.open('API created successfully', 'Close', { duration: 3000 });
570
- } else {
571
- await this.apiService.updateAPI(this.data.api.name, apiData).toPromise();
572
- this.snackBar.open('API updated successfully', 'Close', { duration: 3000 });
573
- }
574
-
575
- this.dialogRef.close(true);
576
- } catch (error: any) {
577
- const message = error.error?.detail ||
578
- (this.data.mode === 'create' ? 'Failed to create API' : 'Failed to update API');
579
- this.snackBar.open(message, 'Close', {
580
- duration: 5000,
581
- panelClass: 'error-snackbar'
582
- });
583
- } finally {
584
- this.saving = false;
585
- }
586
- }
587
-
588
- cancel() {
589
- this.dialogRef.close(false);
590
- }
591
-
592
- saveCursorPosition(field: string, event: any) {
593
- const textarea = event.target;
594
- this.cursorPositions[field] = textarea.selectionStart;
595
- }
596
-
597
- insertTemplateVariable(field: string, variable: string) {
598
- const control = field.includes('.')
599
- ? this.form.get(field)
600
- : this.form.get(field);
601
-
602
- if (control) {
603
- const currentValue = control.value || '';
604
- const variableText = `{{${variable}}}`;
605
- const cursorPos = this.cursorPositions[field] || currentValue.length;
606
-
607
- const newValue =
608
- currentValue.slice(0, cursorPos) +
609
- variableText +
610
- currentValue.slice(cursorPos);
611
-
612
- control.setValue(newValue);
613
-
614
- setTimeout(() => {
615
- let selector = `textarea[formControlName="${field}"]`;
616
- if (field === 'auth.token_request_body') {
617
- selector = 'div[formGroupName="auth"] textarea[formControlName="token_request_body"]';
618
- } else if (field === 'auth.token_refresh_body') {
619
- selector = 'div[formGroupName="auth"] textarea[formControlName="token_refresh_body"]';
620
- }
621
-
622
- const textarea = document.querySelector(selector) as HTMLTextAreaElement;
623
- if (textarea) {
624
- const newPos = cursorPos + variableText.length;
625
- textarea.setSelectionRange(newPos, newPos);
626
- textarea.focus();
627
- }
628
- }, 0);
629
- }
630
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  }
 
1
+ import { Component, Inject, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormArray } from '@angular/forms';
4
+ import { FormsModule } from '@angular/forms';
5
+ import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
6
+ import { MatTabsModule } from '@angular/material/tabs';
7
+ import { MatFormFieldModule } from '@angular/material/form-field';
8
+ import { MatInputModule } from '@angular/material/input';
9
+ import { MatSelectModule } from '@angular/material/select';
10
+ import { MatCheckboxModule } from '@angular/material/checkbox';
11
+ import { MatButtonModule } from '@angular/material/button';
12
+ import { MatIconModule } from '@angular/material/icon';
13
+ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
14
+ import { MatDividerModule } from '@angular/material/divider';
15
+ import { MatExpansionModule } from '@angular/material/expansion';
16
+ import { MatChipsModule } from '@angular/material/chips';
17
+ import { ApiService } from '../../services/api.service';
18
+ import { MatMenuModule } from '@angular/material/menu';
19
+
20
+ @Component({
21
+ selector: 'app-api-edit-dialog',
22
+ standalone: true,
23
+ imports: [
24
+ CommonModule,
25
+ ReactiveFormsModule,
26
+ FormsModule,
27
+ MatDialogModule,
28
+ MatTabsModule,
29
+ MatFormFieldModule,
30
+ MatInputModule,
31
+ MatSelectModule,
32
+ MatCheckboxModule,
33
+ MatButtonModule,
34
+ MatIconModule,
35
+ MatSnackBarModule,
36
+ MatDividerModule,
37
+ MatExpansionModule,
38
+ MatChipsModule,
39
+ MatMenuModule // ✅ MatMenuModule eklendi
40
+ ],
41
+ templateUrl: './api-edit-dialog.component.html',
42
+ styleUrls: ['./api-edit-dialog.component.scss']
43
+ })
44
+ export default class ApiEditDialogComponent implements OnInit {
45
+ form!: FormGroup;
46
+ saving = false;
47
+ testing = false;
48
+ testResult: any = null;
49
+ testRequestJson = '{}';
50
+ allIntentParameters: string[] = [];
51
+ responseMappingVariables: string[] = [];
52
+ activeTabIndex = 0;
53
+
54
+ httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
55
+ retryStrategies = ['static', 'exponential'];
56
+ variableTypes = ['str', 'int', 'float', 'bool', 'date'];
57
+
58
+ private cursorPositions: { [key: string]: number } = {};
59
+
60
+ constructor(
61
+ private fb: FormBuilder,
62
+ private apiService: ApiService,
63
+ private snackBar: MatSnackBar,
64
+ public dialogRef: MatDialogRef<ApiEditDialogComponent>,
65
+ @Inject(MAT_DIALOG_DATA) public data: any
66
+ ) {}
67
+
68
+ ngOnInit() {
69
+ this.initializeForm();
70
+ this.loadIntentParameters();
71
+
72
+ // Aktif tab'ı ayarla
73
+ if (this.data.activeTab !== undefined) {
74
+ this.activeTabIndex = this.data.activeTab;
75
+ }
76
+
77
+ if ((this.data.mode === 'edit' || this.data.mode === 'test') && this.data.api) {
78
+ this.populateForm(this.data.api);
79
+ } else if (this.data.mode === 'duplicate' && this.data.api) {
80
+ const duplicateData = { ...this.data.api };
81
+ duplicateData.name = duplicateData.name + '_copy';
82
+ delete duplicateData.last_update_date;
83
+ this.populateForm(duplicateData);
84
+ }
85
+
86
+ // Test modunda açıldıysa test JSON'ını hazırla
87
+ if (this.data.mode === 'test') {
88
+ setTimeout(() => {
89
+ this.updateTestRequestJson();
90
+ }, 100);
91
+ }
92
+
93
+ // Watch response mappings changes
94
+ this.form.get('response_mappings')?.valueChanges.subscribe(() => {
95
+ this.updateResponseMappingVariables();
96
+ });
97
+
98
+ // JSON alanlarını izle (validation göstergesi için)
99
+ this.form.get('body_template')?.valueChanges.subscribe(() => {
100
+ this.form.get('body_template')?.updateValueAndValidity({ emitEvent: false });
101
+ });
102
+
103
+ this.form.get('auth.token_request_body')?.valueChanges.subscribe(() => {
104
+ this.form.get('auth.token_request_body')?.updateValueAndValidity({ emitEvent: false });
105
+ });
106
+
107
+ this.form.get('auth.token_refresh_body')?.valueChanges.subscribe(() => {
108
+ this.form.get('auth.token_refresh_body')?.updateValueAndValidity({ emitEvent: false });
109
+ });
110
+ }
111
+
112
+ initializeForm() {
113
+ this.form = this.fb.group({
114
+ // General Tab
115
+ name: ['', [Validators.required, Validators.pattern(/^[a-zA-Z0-9_]+$/)]],
116
+ url: ['', [Validators.required, Validators.pattern(/^https?:\/\/.+/)]],
117
+ method: ['POST', Validators.required],
118
+ body_template: ['{}'],
119
+ timeout_seconds: [10, [Validators.required, Validators.min(1), Validators.max(300)]],
120
+ response_prompt: [''],
121
+ response_mappings: this.fb.array([]),
122
+
123
+ // Headers Tab
124
+ headers: this.fb.array([]),
125
+
126
+ // Retry Settings
127
+ retry: this.fb.group({
128
+ retry_count: [3, [Validators.required, Validators.min(0), Validators.max(10)]],
129
+ backoff_seconds: [2, [Validators.required, Validators.min(1), Validators.max(60)]],
130
+ strategy: ['static', Validators.required]
131
+ }),
132
+
133
+ // Auth Tab
134
+ auth: this.fb.group({
135
+ enabled: [false],
136
+ token_endpoint: [''],
137
+ response_token_path: ['token'],
138
+ token_request_body: ['{}'],
139
+ token_refresh_endpoint: [''],
140
+ token_refresh_body: ['{}']
141
+ }),
142
+
143
+ // Proxy (optional)
144
+ proxy: [''],
145
+
146
+ // For race condition handling
147
+ last_update_date: ['']
148
+ });
149
+
150
+ // Watch for auth enabled changes
151
+ this.form.get('auth.enabled')?.valueChanges.subscribe(enabled => {
152
+ const authGroup = this.form.get('auth');
153
+ if (enabled) {
154
+ authGroup?.get('token_endpoint')?.setValidators([Validators.required]);
155
+ authGroup?.get('response_token_path')?.setValidators([Validators.required]);
156
+ } else {
157
+ authGroup?.get('token_endpoint')?.clearValidators();
158
+ authGroup?.get('response_token_path')?.clearValidators();
159
+ }
160
+ authGroup?.get('token_endpoint')?.updateValueAndValidity();
161
+ authGroup?.get('response_token_path')?.updateValueAndValidity();
162
+ });
163
+ }
164
+
165
+ populateForm(api: any) {
166
+ console.log('Populating form with API:', api);
167
+
168
+ // Convert headers object to FormArray
169
+ const headersArray = this.form.get('headers') as FormArray;
170
+ headersArray.clear();
171
+
172
+ if (api.headers) {
173
+ if (Array.isArray(api.headers)) {
174
+ api.headers.forEach((header: any) => {
175
+ headersArray.push(this.createHeaderFormGroup(header.key || '', header.value || ''));
176
+ });
177
+ } else if (typeof api.headers === 'object') {
178
+ Object.entries(api.headers).forEach(([key, value]) => {
179
+ headersArray.push(this.createHeaderFormGroup(key, value as string));
180
+ });
181
+ }
182
+ }
183
+
184
+ // Convert response_mappings to FormArray
185
+ const responseMappingsArray = this.form.get('response_mappings') as FormArray;
186
+ responseMappingsArray.clear();
187
+
188
+ if (api.response_mappings && Array.isArray(api.response_mappings)) {
189
+ api.response_mappings.forEach((mapping: any) => {
190
+ responseMappingsArray.push(this.createResponseMappingFormGroup(mapping));
191
+ });
192
+ }
193
+
194
+ // Convert body_template to JSON string if it's an object
195
+ if (api.body_template && typeof api.body_template === 'object') {
196
+ api.body_template = JSON.stringify(api.body_template, null, 2);
197
+ }
198
+
199
+ // Convert auth bodies to JSON strings
200
+ if (api.auth) {
201
+ if (api.auth.token_request_body && typeof api.auth.token_request_body === 'object') {
202
+ api.auth.token_request_body = JSON.stringify(api.auth.token_request_body, null, 2);
203
+ }
204
+ if (api.auth.token_refresh_body && typeof api.auth.token_refresh_body === 'object') {
205
+ api.auth.token_refresh_body = JSON.stringify(api.auth.token_refresh_body, null, 2);
206
+ }
207
+ }
208
+
209
+ const formData = { ...api };
210
+
211
+ // headers array'ini kaldır çünkü zaten FormArray'e ekledik
212
+ delete formData.headers;
213
+ delete formData.response_mappings;
214
+
215
+ // Patch form values
216
+ this.form.patchValue(formData);
217
+
218
+ // Disable name field if editing or testing
219
+ if (this.data.mode === 'edit' || this.data.mode === 'test') {
220
+ this.form.get('name')?.disable();
221
+ }
222
+ }
223
+
224
+ get headers() {
225
+ return this.form.get('headers') as FormArray;
226
+ }
227
+
228
+ get responseMappings() {
229
+ return this.form.get('response_mappings') as FormArray;
230
+ }
231
+
232
+ createHeaderFormGroup(key = '', value = ''): FormGroup {
233
+ return this.fb.group({
234
+ key: [key, Validators.required],
235
+ value: [value, Validators.required]
236
+ });
237
+ }
238
+
239
+ createResponseMappingFormGroup(data: any = {}): FormGroup {
240
+ return this.fb.group({
241
+ variable_name: [data.variable_name || '', [Validators.required, Validators.pattern(/^[a-z_][a-z0-9_]*$/)]],
242
+ type: [data.type || 'str', Validators.required],
243
+ json_path: [data.json_path || '', Validators.required],
244
+ caption: [data.caption || '', Validators.required]
245
+ });
246
+ }
247
+
248
+ addHeader() {
249
+ this.headers.push(this.createHeaderFormGroup());
250
+ }
251
+
252
+ removeHeader(index: number) {
253
+ this.headers.removeAt(index);
254
+ }
255
+
256
+ addResponseMapping() {
257
+ this.responseMappings.push(this.createResponseMappingFormGroup());
258
+ }
259
+
260
+ removeResponseMapping(index: number) {
261
+ this.responseMappings.removeAt(index);
262
+ }
263
+
264
+ insertHeaderValue(index: number, variable: string) {
265
+ const headerGroup = this.headers.at(index);
266
+ if (headerGroup) {
267
+ const valueControl = headerGroup.get('value');
268
+ if (valueControl) {
269
+ const currentValue = valueControl.value || '';
270
+ const newValue = currentValue + `{{${variable}}}`;
271
+ valueControl.setValue(newValue);
272
+ }
273
+ }
274
+ }
275
+
276
+ getTemplateVariables(includeResponseMappings = true): string[] {
277
+ const variables = new Set<string>();
278
+
279
+ // Intent parameters
280
+ this.allIntentParameters.forEach(param => {
281
+ variables.add(`variables.${param}`);
282
+ });
283
+
284
+ // Auth tokens
285
+ const apiName = this.form.get('name')?.value || 'api_name';
286
+ variables.add(`auth_tokens.${apiName}.token`);
287
+
288
+ // Response mappings
289
+ if (includeResponseMappings) {
290
+ this.responseMappingVariables.forEach(varName => {
291
+ variables.add(`variables.${varName}`);
292
+ });
293
+ }
294
+
295
+ // Config variables
296
+ variables.add('config.work_mode');
297
+ variables.add('config.cloud_token');
298
+
299
+ return Array.from(variables).sort();
300
+ }
301
+
302
+ updateResponseMappingVariables() {
303
+ this.responseMappingVariables = [];
304
+ const mappings = this.responseMappings.value;
305
+ mappings.forEach((mapping: any) => {
306
+ if (mapping.variable_name) {
307
+ this.responseMappingVariables.push(mapping.variable_name);
308
+ }
309
+ });
310
+ }
311
+
312
+ async loadIntentParameters() {
313
+ try {
314
+ const projects = await this.apiService.getProjects(false).toPromise();
315
+ const params = new Set<string>();
316
+
317
+ projects?.forEach(project => {
318
+ project.versions?.forEach(version => {
319
+ version.intents?.forEach(intent => {
320
+ intent.parameters?.forEach((param: any) => {
321
+ if (param.variable_name) {
322
+ params.add(param.variable_name);
323
+ }
324
+ });
325
+ });
326
+ });
327
+ });
328
+
329
+ this.allIntentParameters = Array.from(params).sort();
330
+ } catch (error) {
331
+ console.error('Failed to load intent parameters:', error);
332
+ }
333
+ }
334
+
335
+ validateJSON(field: string): boolean {
336
+ const control = this.form.get(field);
337
+ if (!control || !control.value) return true;
338
+
339
+ try {
340
+ const jsonStr = control.value;
341
+ const processedJson = this.replaceVariablesForValidation(jsonStr);
342
+ JSON.parse(processedJson);
343
+ return true;
344
+ } catch {
345
+ return false;
346
+ }
347
+ }
348
+
349
+ replaceVariablesForValidation(jsonStr: string): string {
350
+ let processed = jsonStr;
351
+
352
+ processed = processed.replace(/\{\{([^}]+)\}\}/g, (match, variablePath) => {
353
+ if (variablePath.includes('variables.')) {
354
+ const varName = variablePath.split('.').pop()?.toLowerCase() || '';
355
+
356
+ const numericVars = ['count', 'passenger_count', 'timeout_seconds', 'retry_count', 'amount', 'price', 'quantity', 'age', 'id'];
357
+ const booleanVars = ['enabled', 'published', 'is_active', 'confirmed', 'canceled', 'deleted', 'required'];
358
+
359
+ if (numericVars.some(v => varName.includes(v))) {
360
+ return '1';
361
+ } else if (booleanVars.some(v => varName.includes(v))) {
362
+ return 'true';
363
+ } else {
364
+ return '"placeholder"';
365
+ }
366
+ }
367
+
368
+ return '"placeholder"';
369
+ });
370
+
371
+ return processed;
372
+ }
373
+
374
+ async testAPI() {
375
+ const generalValid = this.form.get('url')?.valid && this.form.get('method')?.valid;
376
+ if (!generalValid) {
377
+ this.snackBar.open('Please fill in required fields first', 'Close', { duration: 3000 });
378
+ return;
379
+ }
380
+
381
+ this.testing = true;
382
+ this.testResult = null;
383
+
384
+ try {
385
+ const testData = this.prepareAPIData();
386
+
387
+ let testRequestData = {};
388
+ try {
389
+ testRequestData = JSON.parse(this.testRequestJson);
390
+ } catch (e) {
391
+ this.snackBar.open('Invalid test request JSON', 'Close', {
392
+ duration: 3000,
393
+ panelClass: 'error-snackbar'
394
+ });
395
+ this.testing = false;
396
+ return;
397
+ }
398
+
399
+ testData.test_request = testRequestData;
400
+
401
+ const result = await this.apiService.testAPI(testData).toPromise();
402
+ this.testResult = result;
403
+
404
+ if (result.success) {
405
+ this.snackBar.open(`API test successful! (${result.status_code})`, 'Close', {
406
+ duration: 3000
407
+ });
408
+ } else {
409
+ const errorMsg = result.error || `API returned status ${result.status_code}`;
410
+ this.snackBar.open(`API test failed: ${errorMsg}`, 'Close', {
411
+ duration: 5000,
412
+ panelClass: 'error-snackbar'
413
+ });
414
+ }
415
+ } catch (error: any) {
416
+ this.testResult = {
417
+ success: false,
418
+ error: error.message || 'Test failed'
419
+ };
420
+ this.snackBar.open('API test failed', 'Close', {
421
+ duration: 3000,
422
+ panelClass: 'error-snackbar'
423
+ });
424
+ } finally {
425
+ this.testing = false;
426
+ }
427
+ }
428
+
429
+ prepareTestRequest(): any {
430
+ try {
431
+ const requestData = JSON.parse(this.testRequestJson);
432
+ return requestData;
433
+ } catch {
434
+ return {};
435
+ }
436
+ }
437
+
438
+ updateTestRequestJson() {
439
+ const formValue = this.form.getRawValue();
440
+ let bodyTemplate = {};
441
+
442
+ try {
443
+ bodyTemplate = JSON.parse(formValue.body_template);
444
+ } catch {
445
+ bodyTemplate = {};
446
+ }
447
+
448
+ const testData = this.replacePlaceholdersForTest(bodyTemplate);
449
+ this.testRequestJson = JSON.stringify(testData, null, 2);
450
+ }
451
+
452
+ replacePlaceholdersForTest(obj: any): any {
453
+ if (typeof obj === 'string') {
454
+ let result = obj;
455
+
456
+ result = result.replace(/\{\{variables\.origin\}\}/g, 'Istanbul');
457
+ result = result.replace(/\{\{variables\.destination\}\}/g, 'Ankara');
458
+ result = result.replace(/\{\{variables\.flight_date\}\}/g, '2025-06-15');
459
+ result = result.replace(/\{\{variables\.passenger_count\}\}/g, '2');
460
+ result = result.replace(/\{\{variables\.flight_number\}\}/g, 'TK123');
461
+ result = result.replace(/\{\{variables\.pnr\}\}/g, 'ABC12');
462
+ result = result.replace(/\{\{variables\.surname\}\}/g, 'Test');
463
+
464
+ result = result.replace(/\{\{auth_tokens\.[^}]+\.token\}\}/g, 'test_token_123');
465
+
466
+ result = result.replace(/\{\{config\.work_mode\}\}/g, 'hfcloud');
467
+
468
+ result = result.replace(/\{\{[^}]+\}\}/g, 'test_value');
469
+
470
+ return result;
471
+ } else if (typeof obj === 'object' && obj !== null) {
472
+ const result: any = Array.isArray(obj) ? [] : {};
473
+ for (const key in obj) {
474
+ result[key] = this.replacePlaceholdersForTest(obj[key]);
475
+ }
476
+ return result;
477
+ }
478
+ return obj;
479
+ }
480
+
481
+ prepareAPIData(): any {
482
+ const formValue = this.form.getRawValue();
483
+
484
+ const headers: any = {};
485
+ formValue.headers.forEach((h: any) => {
486
+ if (h.key && h.value) {
487
+ headers[h.key] = h.value;
488
+ }
489
+ });
490
+
491
+ let body_template = {};
492
+ let auth_token_request_body = {};
493
+ let auth_token_refresh_body = {};
494
+
495
+ try {
496
+ body_template = formValue.body_template ? JSON.parse(formValue.body_template) : {};
497
+ } catch (e) {
498
+ console.error('Invalid body_template JSON:', e);
499
+ }
500
+
501
+ try {
502
+ auth_token_request_body = formValue.auth.token_request_body ? JSON.parse(formValue.auth.token_request_body) : {};
503
+ } catch (e) {
504
+ console.error('Invalid auth token_request_body JSON:', e);
505
+ }
506
+
507
+ try {
508
+ auth_token_refresh_body = formValue.auth.token_refresh_body ? JSON.parse(formValue.auth.token_refresh_body) : {};
509
+ } catch (e) {
510
+ console.error('Invalid auth token_refresh_body JSON:', e);
511
+ }
512
+
513
+ const apiData: any = {
514
+ name: formValue.name,
515
+ url: formValue.url,
516
+ method: formValue.method,
517
+ headers,
518
+ body_template,
519
+ timeout_seconds: formValue.timeout_seconds,
520
+ retry: formValue.retry,
521
+ response_prompt: formValue.response_prompt,
522
+ response_mappings: formValue.response_mappings || []
523
+ };
524
+
525
+ if (formValue.proxy) {
526
+ apiData.proxy = formValue.proxy;
527
+ }
528
+
529
+ if (formValue.auth.enabled) {
530
+ apiData.auth = {
531
+ enabled: true,
532
+ token_endpoint: formValue.auth.token_endpoint,
533
+ response_token_path: formValue.auth.response_token_path,
534
+ token_request_body: auth_token_request_body
535
+ };
536
+
537
+ if (formValue.auth.token_refresh_endpoint) {
538
+ apiData.auth.token_refresh_endpoint = formValue.auth.token_refresh_endpoint;
539
+ apiData.auth.token_refresh_body = auth_token_refresh_body;
540
+ }
541
+ }
542
+
543
+ if (this.data.mode === 'edit' && formValue.last_update_date) {
544
+ apiData.last_update_date = formValue.last_update_date;
545
+ }
546
+
547
+ return apiData;
548
+ }
549
+
550
+ async save() {
551
+ if (this.data.mode === 'test') {
552
+ this.cancel();
553
+ return;
554
+ }
555
+
556
+ if (this.form.invalid) {
557
+ Object.keys(this.form.controls).forEach(key => {
558
+ this.form.get(key)?.markAsTouched();
559
+ });
560
+
561
+ const jsonFields = ['body_template', 'auth.token_request_body', 'auth.token_refresh_body'];
562
+ for (const field of jsonFields) {
563
+ if (!this.validateJSON(field)) {
564
+ this.snackBar.open(`Invalid JSON in ${field}`, 'Close', {
565
+ duration: 3000,
566
+ panelClass: 'error-snackbar'
567
+ });
568
+ return;
569
+ }
570
+ }
571
+
572
+ this.snackBar.open('Please fix validation errors', 'Close', { duration: 3000 });
573
+ return;
574
+ }
575
+
576
+ this.saving = true;
577
+ try {
578
+ const apiData = this.prepareAPIData();
579
+
580
+ if (this.data.mode === 'create' || this.data.mode === 'duplicate') {
581
+ await this.apiService.createAPI(apiData).toPromise();
582
+ this.snackBar.open('API created successfully', 'Close', { duration: 3000 });
583
+ } else {
584
+ await this.apiService.updateAPI(this.data.api.name, apiData).toPromise();
585
+ this.snackBar.open('API updated successfully', 'Close', { duration: 3000 });
586
+ }
587
+
588
+ this.dialogRef.close(true);
589
+ } catch (error: any) {
590
+ const message = error.error?.detail ||
591
+ (this.data.mode === 'create' ? 'Failed to create API' : 'Failed to update API');
592
+ this.snackBar.open(message, 'Close', {
593
+ duration: 5000,
594
+ panelClass: 'error-snackbar'
595
+ });
596
+ } finally {
597
+ this.saving = false;
598
+ }
599
+ }
600
+
601
+ cancel() {
602
+ this.dialogRef.close(false);
603
+ }
604
+
605
+ saveCursorPosition(field: string, event: any) {
606
+ const textarea = event.target;
607
+ this.cursorPositions[field] = textarea.selectionStart;
608
+ }
609
+
610
+ insertTemplateVariable(field: string, variable: string) {
611
+ const control = field.includes('.')
612
+ ? this.form.get(field)
613
+ : this.form.get(field);
614
+
615
+ if (control) {
616
+ const currentValue = control.value || '';
617
+ const variableText = `{{${variable}}}`;
618
+ const cursorPos = this.cursorPositions[field] || currentValue.length;
619
+
620
+ const newValue =
621
+ currentValue.slice(0, cursorPos) +
622
+ variableText +
623
+ currentValue.slice(cursorPos);
624
+
625
+ control.setValue(newValue);
626
+
627
+ setTimeout(() => {
628
+ let selector = `textarea[formControlName="${field}"]`;
629
+ if (field === 'auth.token_request_body') {
630
+ selector = 'div[formGroupName="auth"] textarea[formControlName="token_request_body"]';
631
+ } else if (field === 'auth.token_refresh_body') {
632
+ selector = 'div[formGroupName="auth"] textarea[formControlName="token_refresh_body"]';
633
+ }
634
+
635
+ const textarea = document.querySelector(selector) as HTMLTextAreaElement;
636
+ if (textarea) {
637
+ const newPos = cursorPos + variableText.length;
638
+ textarea.setSelectionRange(newPos, newPos);
639
+ textarea.focus();
640
+ }
641
+ }, 0);
642
+ }
643
+ }
644
  }