ciyidogan commited on
Commit
ac0ed62
·
verified ·
1 Parent(s): 80992be

Upload 3 files

Browse files
flare-ui/src/app/dialogs/api-edit-dialog/api-edit-dialog.component.html CHANGED
@@ -1,469 +1,469 @@
1
- <h2 mat-dialog-title>
2
- @if (data.mode === 'create') {
3
- Create New API
4
- } @else if (data.mode === 'duplicate') {
5
- Duplicate API
6
- } @else if (data.mode === 'test') {
7
- Test API: {{ data.api.name }}
8
- } @else {
9
- Edit API: {{ data.api.name }}
10
- }
11
- </h2>
12
-
13
- <mat-dialog-content>
14
- <mat-tab-group [(selectedIndex)]="activeTabIndex">
15
- <!-- General Tab -->
16
- <mat-tab label="General">
17
- <div class="tab-content">
18
- <mat-form-field appearance="outline">
19
- <mat-label>Name</mat-label>
20
- <input matInput [formControl]="$any(form.get('name'))" placeholder="e.g., get_flights">
21
- <mat-hint>Unique identifier for this API</mat-hint>
22
- @if (form.get('name')?.hasError('required') && form.get('name')?.touched) {
23
- <mat-error>Name is required</mat-error>
24
- }
25
- @if (form.get('name')?.hasError('pattern') && form.get('name')?.touched) {
26
- <mat-error>Only alphanumeric and underscore allowed</mat-error>
27
- }
28
- </mat-form-field>
29
-
30
- <mat-form-field appearance="outline">
31
- <mat-label>URL</mat-label>
32
- <input matInput [formControl]="$any(form.get('url'))" placeholder="https://api.example.com/endpoint">
33
- <mat-hint>Full URL including protocol</mat-hint>
34
- @if (form.get('url')?.hasError('required') && form.get('url')?.touched) {
35
- <mat-error>URL is required</mat-error>
36
- }
37
- @if (form.get('url')?.hasError('pattern') && form.get('url')?.touched) {
38
- <mat-error>Invalid URL format</mat-error>
39
- }
40
- </mat-form-field>
41
-
42
- <div class="row">
43
- <mat-form-field appearance="outline" class="method-field">
44
- <mat-label>Method</mat-label>
45
- <mat-select [formControl]="$any(form.get('method'))">
46
- @for (method of httpMethods; track method) {
47
- <mat-option [value]="method">{{ method }}</mat-option>
48
- }
49
- </mat-select>
50
- </mat-form-field>
51
-
52
- <mat-form-field appearance="outline" class="timeout-field">
53
- <mat-label>Timeout (seconds)</mat-label>
54
- <input matInput type="number" [formControl]="$any(form.get('timeout_seconds'))">
55
- <mat-hint>Request timeout in seconds</mat-hint>
56
- @if (form.get('timeout_seconds')?.hasError('min')) {
57
- <mat-error>Minimum 1 second</mat-error>
58
- }
59
- @if (form.get('timeout_seconds')?.hasError('max')) {
60
- <mat-error>Maximum 300 seconds</mat-error>
61
- }
62
- </mat-form-field>
63
- </div>
64
-
65
- <mat-form-field appearance="outline" class="full-width">
66
- <mat-label>Body Template</mat-label>
67
- <textarea matInput
68
- [formControl]="$any(form.get('body_template'))"
69
- rows="8"
70
- placeholder='{"key": "value"}'
71
- (click)="saveCursorPosition('body_template', $event)"
72
- (keyup)="saveCursorPosition('body_template', $event)"></textarea>
73
- <mat-hint>JSON template with template variable support</mat-hint>
74
- @if (!validateJSON('body_template')) {
75
- <mat-error>Invalid JSON format</mat-error>
76
- }
77
- </mat-form-field>
78
-
79
- <div class="template-variables">
80
- <strong>Available Variables:</strong>
81
- <mat-chip-set>
82
- @for (variable of getTemplateVariables(false); track variable) {
83
- <mat-chip (click)="insertTemplateVariable('body_template', variable)">
84
- {{ variable }}
85
- </mat-chip>
86
- }
87
- </mat-chip-set>
88
- </div>
89
-
90
- <mat-form-field appearance="outline">
91
- <mat-label>Proxy URL (Optional)</mat-label>
92
- <input matInput [formControl]="$any(form.get('proxy'))" placeholder="http://proxy.example.com:8080">
93
- <mat-hint>HTTP proxy for this API call</mat-hint>
94
- </mat-form-field>
95
- </div>
96
- </mat-tab>
97
-
98
- <!-- Headers Tab -->
99
- <mat-tab label="Headers">
100
- <div class="tab-content">
101
- <div class="array-section">
102
- <div class="section-header">
103
- <h3>Request Headers</h3>
104
- <button mat-button color="primary" (click)="addHeader()">
105
- <mat-icon>add</mat-icon>
106
- Add Header
107
- </button>
108
- </div>
109
-
110
- @if (headers.length === 0) {
111
- <p class="empty-message">No headers configured. Click "Add Header" to add one.</p>
112
- }
113
-
114
- @for (header of headers.controls; track header; let i = $index) {
115
- <div class="array-item" [formGroup]="$any(header)">
116
- <mat-form-field appearance="outline" class="key-field">
117
- <mat-label>Header Name</mat-label>
118
- <input matInput formControlName="key" placeholder="Content-Type">
119
- </mat-form-field>
120
-
121
- <mat-form-field appearance="outline" class="value-field">
122
- <mat-label>Header Value</mat-label>
123
- <input matInput formControlName="value" placeholder="application/json">
124
- <button mat-icon-button matSuffix [matMenuTriggerFor]="headerMenu">
125
- <mat-icon>code</mat-icon>
126
- </button>
127
- <mat-menu #headerMenu="matMenu">
128
- @for (variable of getTemplateVariables(); track variable) {
129
- <button mat-menu-item (click)="insertHeaderValue(i, variable)">
130
- {{ variable }}
131
- </button>
132
- }
133
- </mat-menu>
134
- </mat-form-field>
135
-
136
- <button mat-icon-button color="warn" (click)="removeHeader(i)">
137
- <mat-icon>delete</mat-icon>
138
- </button>
139
- </div>
140
- }
141
- </div>
142
- </div>
143
- </mat-tab>
144
-
145
- <!-- Response Tab -->
146
- <mat-tab label="Response">
147
- <div class="tab-content">
148
- <mat-form-field appearance="outline" class="full-width">
149
- <mat-label>Response Prompt</mat-label>
150
- <textarea matInput
151
- [formControl]="$any(form.get('response_prompt'))"
152
- rows="4"
153
- placeholder="Optional instructions for processing the response"></textarea>
154
- <mat-hint>Instructions for AI to process the response (optional)</mat-hint>
155
- </mat-form-field>
156
-
157
- <mat-divider></mat-divider>
158
-
159
- <div class="array-section">
160
- <div class="section-header">
161
- <h3>Response Mappings</h3>
162
- <button mat-button color="primary" (click)="addResponseMapping()">
163
- <mat-icon>add</mat-icon>
164
- Add Mapping
165
- </button>
166
- </div>
167
-
168
- <p class="info-text">
169
- Extract values from API response and save them as variables for use in subsequent intents.
170
- </p>
171
-
172
- @if (responseMappings.length === 0) {
173
- <p class="empty-message">No response mappings configured.</p>
174
- }
175
-
176
- @for (mapping of responseMappings.controls; track mapping; let i = $index) {
177
- <mat-expansion-panel [formGroup]="$any(mapping)">
178
- <mat-expansion-panel-header>
179
- <mat-panel-title>
180
- {{ mapping.get('variable_name')?.value || 'New Mapping' }}
181
- </mat-panel-title>
182
- <mat-panel-description>
183
- {{ mapping.get('json_path')?.value || 'Configure mapping' }}
184
- </mat-panel-description>
185
- </mat-expansion-panel-header>
186
-
187
- <div class="mapping-content">
188
- <mat-form-field appearance="outline">
189
- <mat-label>Variable Name</mat-label>
190
- <input matInput formControlName="variable_name" placeholder="booking_ref">
191
- <mat-hint>Name to store the extracted value</mat-hint>
192
- @if (mapping.get('variable_name')?.hasError('pattern')) {
193
- <mat-error>Lowercase letters, numbers and underscore only</mat-error>
194
- }
195
- </mat-form-field>
196
-
197
- <mat-form-field appearance="outline">
198
- <mat-label>Caption</mat-label>
199
- <input matInput formControlName="caption" placeholder="Booking Reference">
200
- <mat-hint>Human-readable description</mat-hint>
201
- </mat-form-field>
202
-
203
- <div class="row">
204
- <mat-form-field appearance="outline" class="type-field">
205
- <mat-label>Type</mat-label>
206
- <mat-select formControlName="type">
207
- @for (type of variableTypes; track type) {
208
- <mat-option [value]="type">{{ type }}</mat-option>
209
- }
210
- </mat-select>
211
- </mat-form-field>
212
-
213
- <mat-form-field appearance="outline" class="path-field">
214
- <mat-label>JSON Path</mat-label>
215
- <input matInput formControlName="json_path" placeholder="$.data.bookingReference">
216
- <mat-hint>JSONPath expression to extract value</mat-hint>
217
- </mat-form-field>
218
- </div>
219
-
220
- <button mat-button color="warn" (click)="removeResponseMapping(i)">
221
- <mat-icon>delete</mat-icon>
222
- Remove Mapping
223
- </button>
224
- </div>
225
- </mat-expansion-panel>
226
- }
227
- </div>
228
-
229
- <!-- Retry Settings -->
230
- <mat-divider></mat-divider>
231
-
232
- <div class="retry-section" [formGroup]="$any(form.get('retry'))">
233
- <h3>Retry Settings</h3>
234
-
235
- <div class="row">
236
- <mat-form-field appearance="outline">
237
- <mat-label>Retry Count</mat-label>
238
- <input matInput type="number" formControlName="retry_count">
239
- <mat-hint>Number of retry attempts</mat-hint>
240
- </mat-form-field>
241
-
242
- <mat-form-field appearance="outline">
243
- <mat-label>Backoff (seconds)</mat-label>
244
- <input matInput type="number" formControlName="backoff_seconds">
245
- <mat-hint>Delay between retries</mat-hint>
246
- </mat-form-field>
247
-
248
- <mat-form-field appearance="outline">
249
- <mat-label>Strategy</mat-label>
250
- <mat-select formControlName="strategy">
251
- @for (strategy of retryStrategies; track strategy) {
252
- <mat-option [value]="strategy">{{ strategy }}</mat-option>
253
- }
254
- </mat-select>
255
- </mat-form-field>
256
- </div>
257
- </div>
258
- </div>
259
- </mat-tab>
260
-
261
- <!-- Test Tab -->
262
- <mat-tab label="Test">
263
- <div class="tab-content">
264
- <div class="test-section">
265
- <h3>Test API Call</h3>
266
-
267
- <div class="test-controls">
268
- <button mat-raised-button color="primary"
269
- (click)="testAPI()"
270
- [disabled]="testing || !form.get('url')?.valid || !form.get('method')?.valid">
271
- @if (testing) {
272
- <ng-container>
273
- <mat-icon class="spin">sync</mat-icon>
274
- Testing...
275
- </ng-container>
276
- } @else {
277
- <ng-container>
278
- <mat-icon>play_arrow</mat-icon>
279
- Test API
280
- </ng-container>
281
- }
282
- </button>
283
-
284
- <button mat-button (click)="updateTestRequestJson()">
285
- <mat-icon>refresh</mat-icon>
286
- Generate Test Data
287
- </button>
288
- </div>
289
-
290
- <mat-form-field appearance="outline" class="full-width">
291
- <mat-label>Test Request Body</mat-label>
292
- <textarea matInput
293
- [(ngModel)]="testRequestJson"
294
- rows="10"
295
- placeholder="Enter test request JSON here"></textarea>
296
- <mat-hint>Variables will be replaced with test values</mat-hint>
297
- </mat-form-field>
298
-
299
- @if (testResult) {
300
- <mat-divider></mat-divider>
301
-
302
- <div class="test-result" [class.success]="testResult.success" [class.error]="!testResult.success">
303
- <h4>Test Result</h4>
304
-
305
- @if (testResult.success) {
306
- <div class="result-status">
307
- <mat-icon>check_circle</mat-icon>
308
- <span>Success ({{ testResult.status_code }})</span>
309
- </div>
310
- } @else {
311
- <div class="result-status">
312
- <mat-icon>error</mat-icon>
313
- <span>Failed: {{ testResult.error }}</span>
314
- </div>
315
- }
316
-
317
- @if (testResult.response_time) {
318
- <p><strong>Response Time:</strong> {{ testResult.response_time }}ms</p>
319
- }
320
-
321
- @if (testResult.response_body) {
322
- <mat-form-field appearance="outline" class="full-width">
323
- <mat-label>Response Body</mat-label>
324
- <textarea matInput
325
- [value]="testResult.response_body | json"
326
- rows="10"
327
- readonly></textarea>
328
- </mat-form-field>
329
- }
330
-
331
- @if (testResult.request_body) {
332
- <mat-form-field appearance="outline" class="full-width">
333
- <mat-label>Actual Request Sent</mat-label>
334
- <textarea matInput
335
- [value]="testResult.request_body | json"
336
- rows="8"
337
- readonly></textarea>
338
- </mat-form-field>
339
- }
340
- </div>
341
- }
342
- </div>
343
- </div>
344
- </mat-tab>
345
-
346
- <!-- Auth Tab -->
347
- <mat-tab label="Authentication">
348
- <div class="tab-content" [formGroup]="$any(form.get('auth'))">
349
- <mat-checkbox formControlName="enabled">
350
- Enable Authentication
351
- </mat-checkbox>
352
-
353
- @if (form.get('auth.enabled')?.value) {
354
- <mat-divider></mat-divider>
355
-
356
- <div class="auth-section">
357
- <h3>Token Configuration</h3>
358
-
359
- <mat-form-field appearance="outline">
360
- <mat-label>Token Endpoint</mat-label>
361
- <input matInput formControlName="token_endpoint" placeholder="https://api.example.com/auth/token">
362
- <mat-hint>URL to obtain authentication token</mat-hint>
363
- @if (form.get('auth.token_endpoint')?.hasError('required') && form.get('auth.token_endpoint')?.touched) {
364
- <mat-error>Token endpoint is required when auth is enabled</mat-error>
365
- }
366
- </mat-form-field>
367
-
368
- <mat-form-field appearance="outline">
369
- <mat-label>Token Response Path</mat-label>
370
- <input matInput formControlName="response_token_path" placeholder="token">
371
- <mat-hint>JSON path to extract token from response</mat-hint>
372
- @if (form.get('auth.response_token_path')?.hasError('required') && form.get('auth.response_token_path')?.touched) {
373
- <mat-error>Token path is required when auth is enabled</mat-error>
374
- }
375
- </mat-form-field>
376
-
377
- <mat-form-field appearance="outline" class="full-width">
378
- <mat-label>Token Request Body</mat-label>
379
- <textarea matInput
380
- formControlName="token_request_body"
381
- rows="6"
382
- (click)="saveCursorPosition('auth.token_request_body', $event)"
383
- (keyup)="saveCursorPosition('auth.token_request_body', $event)"
384
- placeholder='{"username": "api_user", "password": "api_pass"}'></textarea>
385
- <mat-hint>JSON body for token request</mat-hint>
386
- @if (!validateJSON('auth.token_request_body')) {
387
- <mat-error>Invalid JSON format</mat-error>
388
- }
389
- </mat-form-field>
390
-
391
- <div class="template-variables">
392
- <strong>Available Variables:</strong>
393
- <mat-chip-set>
394
- @for (variable of getTemplateVariables(); track variable) {
395
- <mat-chip (click)="insertTemplateVariable('auth.token_request_body', variable)">
396
- {{ variable }}
397
- </mat-chip>
398
- }
399
- </mat-chip-set>
400
- </div>
401
-
402
- <mat-divider></mat-divider>
403
-
404
- <h3>Token Refresh (Optional)</h3>
405
-
406
- <mat-form-field appearance="outline">
407
- <mat-label>Refresh Endpoint</mat-label>
408
- <input matInput formControlName="token_refresh_endpoint" placeholder="https://api.example.com/auth/refresh">
409
- <mat-hint>URL to refresh expired token</mat-hint>
410
- </mat-form-field>
411
-
412
- <mat-form-field appearance="outline" class="full-width">
413
- <mat-label>Refresh Request Body</mat-label>
414
- <textarea matInput
415
- formControlName="token_refresh_body"
416
- rows="4"
417
- (click)="saveCursorPosition('auth.token_refresh_body', $event)"
418
- (keyup)="saveCursorPosition('auth.token_refresh_body', $event)"
419
- placeholder='{"refresh_token": "your_refresh_token"}'></textarea>
420
- <mat-hint>JSON body for refresh request</mat-hint>
421
- @if (!validateJSON('auth.token_refresh_body')) {
422
- <mat-error>Invalid JSON format</mat-error>
423
- }
424
- </mat-form-field>
425
-
426
- <div class="template-variables">
427
- <strong>Available Variables:</strong>
428
- <mat-chip-set>
429
- @for (variable of getTemplateVariables(); track variable) {
430
- <mat-chip (click)="insertTemplateVariable('auth.token_refresh_body', variable)">
431
- {{ variable }}
432
- </mat-chip>
433
- }
434
- </mat-chip-set>
435
- </div>
436
- </div>
437
- }
438
- </div>
439
- </mat-tab>
440
- </mat-tab-group>
441
- </mat-dialog-content>
442
-
443
- <mat-dialog-actions align="end">
444
- <button mat-button (click)="cancel()">
445
- @if (data.mode === 'test') {
446
- Close
447
- } @else {
448
- Cancel
449
- }
450
- </button>
451
- @if (data.mode !== 'test') {
452
- <button mat-raised-button color="primary"
453
- (click)="save()"
454
- [disabled]="saving || form.invalid">
455
- @if (saving) {
456
- <ng-container>
457
- <mat-icon class="spin">sync</mat-icon>
458
- Saving...
459
- </ng-container>
460
- } @else {
461
- @if (data.mode === 'create' || data.mode === 'duplicate') {
462
- Create
463
- } @else {
464
- Update
465
- }
466
- }
467
- </button>
468
- }
469
  </mat-dialog-actions>
 
1
+ <h2 mat-dialog-title>
2
+ @if (data.mode === 'create') {
3
+ Create New API
4
+ } @else if (data.mode === 'duplicate') {
5
+ Duplicate API
6
+ } @else if (data.mode === 'test') {
7
+ Test API: {{ data.api.name }}
8
+ } @else {
9
+ Edit API: {{ data.api.name }}
10
+ }
11
+ </h2>
12
+
13
+ <mat-dialog-content>
14
+ <mat-tab-group [(selectedIndex)]="activeTabIndex">
15
+ <!-- General Tab -->
16
+ <mat-tab label="General">
17
+ <div class="tab-content">
18
+ <mat-form-field appearance="outline">
19
+ <mat-label>Name</mat-label>
20
+ <input matInput [formControl]="$any(form.get('name'))" placeholder="e.g., get_flights">
21
+ <mat-hint>Unique identifier for this API</mat-hint>
22
+ @if (form.get('name')?.hasError('required') && form.get('name')?.touched) {
23
+ <mat-error>Name is required</mat-error>
24
+ }
25
+ @if (form.get('name')?.hasError('pattern') && form.get('name')?.touched) {
26
+ <mat-error>Only alphanumeric and underscore allowed</mat-error>
27
+ }
28
+ </mat-form-field>
29
+
30
+ <mat-form-field appearance="outline">
31
+ <mat-label>URL</mat-label>
32
+ <input matInput [formControl]="$any(form.get('url'))" placeholder="https://api.example.com/endpoint">
33
+ <mat-hint>Full URL including protocol</mat-hint>
34
+ @if (form.get('url')?.hasError('required') && form.get('url')?.touched) {
35
+ <mat-error>URL is required</mat-error>
36
+ }
37
+ @if (form.get('url')?.hasError('pattern') && form.get('url')?.touched) {
38
+ <mat-error>Invalid URL format</mat-error>
39
+ }
40
+ </mat-form-field>
41
+
42
+ <div class="row">
43
+ <mat-form-field appearance="outline" class="method-field">
44
+ <mat-label>Method</mat-label>
45
+ <mat-select [formControl]="$any(form.get('method'))">
46
+ @for (method of httpMethods; track method) {
47
+ <mat-option [value]="method">{{ method }}</mat-option>
48
+ }
49
+ </mat-select>
50
+ </mat-form-field>
51
+
52
+ <mat-form-field appearance="outline" class="timeout-field">
53
+ <mat-label>Timeout (seconds)</mat-label>
54
+ <input matInput type="number" [formControl]="$any(form.get('timeout_seconds'))">
55
+ <mat-hint>Request timeout in seconds</mat-hint>
56
+ @if (form.get('timeout_seconds')?.hasError('min')) {
57
+ <mat-error>Minimum 1 second</mat-error>
58
+ }
59
+ @if (form.get('timeout_seconds')?.hasError('max')) {
60
+ <mat-error>Maximum 300 seconds</mat-error>
61
+ }
62
+ </mat-form-field>
63
+ </div>
64
+
65
+ <mat-form-field appearance="outline" class="full-width">
66
+ <mat-label>Body Template</mat-label>
67
+ <textarea matInput
68
+ [formControl]="$any(form.get('body_template'))"
69
+ rows="8"
70
+ placeholder='{"key": "value"}'
71
+ (click)="saveCursorPosition('body_template', $event)"
72
+ (keyup)="saveCursorPosition('body_template', $event)"></textarea>
73
+ <mat-hint>JSON template with template variable support</mat-hint>
74
+ @if (!validateJSON('body_template')) {
75
+ <mat-error>Invalid JSON format</mat-error>
76
+ }
77
+ </mat-form-field>
78
+
79
+ <div class="template-variables">
80
+ <strong>Available Variables:</strong>
81
+ <mat-chip-set>
82
+ @for (variable of getTemplateVariables(false); track variable) {
83
+ <mat-chip (click)="insertTemplateVariable('body_template', variable)">
84
+ {{ variable }}
85
+ </mat-chip>
86
+ }
87
+ </mat-chip-set>
88
+ </div>
89
+
90
+ <mat-form-field appearance="outline">
91
+ <mat-label>Proxy URL (Optional)</mat-label>
92
+ <input matInput [formControl]="$any(form.get('proxy'))" placeholder="http://proxy.example.com:8080">
93
+ <mat-hint>HTTP proxy for this API call</mat-hint>
94
+ </mat-form-field>
95
+ </div>
96
+ </mat-tab>
97
+
98
+ <!-- Headers Tab -->
99
+ <mat-tab label="Headers">
100
+ <div class="tab-content">
101
+ <div class="array-section">
102
+ <div class="section-header">
103
+ <h3>Request Headers</h3>
104
+ <button mat-button color="primary" (click)="addHeader()">
105
+ <mat-icon>add</mat-icon>
106
+ Add Header
107
+ </button>
108
+ </div>
109
+
110
+ @if (headers.length === 0) {
111
+ <p class="empty-message">No headers configured. Click "Add Header" to add one.</p>
112
+ }
113
+
114
+ @for (header of headers.controls; track header; let i = $index) {
115
+ <div class="array-item" [formGroup]="$any(header)">
116
+ <mat-form-field appearance="outline" class="key-field">
117
+ <mat-label>Header Name</mat-label>
118
+ <input matInput formControlName="key" placeholder="Content-Type">
119
+ </mat-form-field>
120
+
121
+ <mat-form-field appearance="outline" class="value-field">
122
+ <mat-label>Header Value</mat-label>
123
+ <input matInput formControlName="value" placeholder="application/json">
124
+ <button mat-icon-button matSuffix [matMenuTriggerFor]="headerMenu">
125
+ <mat-icon>code</mat-icon>
126
+ </button>
127
+ <mat-menu #headerMenu="matMenu">
128
+ @for (variable of getTemplateVariables(); track variable) {
129
+ <button mat-menu-item (click)="insertHeaderValue(i, variable)">
130
+ {{ variable }}
131
+ </button>
132
+ }
133
+ </mat-menu>
134
+ </mat-form-field>
135
+
136
+ <button mat-icon-button color="warn" (click)="removeHeader(i)">
137
+ <mat-icon>delete</mat-icon>
138
+ </button>
139
+ </div>
140
+ }
141
+ </div>
142
+ </div>
143
+ </mat-tab>
144
+
145
+ <!-- Response Tab -->
146
+ <mat-tab label="Response">
147
+ <div class="tab-content">
148
+ <mat-form-field appearance="outline" class="full-width">
149
+ <mat-label>Response Prompt</mat-label>
150
+ <textarea matInput
151
+ [formControl]="$any(form.get('response_prompt'))"
152
+ rows="4"
153
+ placeholder="Optional instructions for processing the response"></textarea>
154
+ <mat-hint>Instructions for AI to process the response (optional)</mat-hint>
155
+ </mat-form-field>
156
+
157
+ <mat-divider></mat-divider>
158
+
159
+ <div class="array-section">
160
+ <div class="section-header">
161
+ <h3>Response Mappings</h3>
162
+ <button mat-button color="primary" (click)="addResponseMapping()">
163
+ <mat-icon>add</mat-icon>
164
+ Add Mapping
165
+ </button>
166
+ </div>
167
+
168
+ <p class="info-text">
169
+ Extract values from API response and save them as variables for use in subsequent intents.
170
+ </p>
171
+
172
+ @if (responseMappings.length === 0) {
173
+ <p class="empty-message">No response mappings configured.</p>
174
+ }
175
+
176
+ @for (mapping of responseMappings.controls; track mapping; let i = $index) {
177
+ <mat-expansion-panel [formGroup]="$any(mapping)">
178
+ <mat-expansion-panel-header>
179
+ <mat-panel-title>
180
+ {{ mapping.get('variable_name')?.value || 'New Mapping' }}
181
+ </mat-panel-title>
182
+ <mat-panel-description>
183
+ {{ mapping.get('json_path')?.value || 'Configure mapping' }}
184
+ </mat-panel-description>
185
+ </mat-expansion-panel-header>
186
+
187
+ <div class="mapping-content">
188
+ <mat-form-field appearance="outline">
189
+ <mat-label>Variable Name</mat-label>
190
+ <input matInput formControlName="variable_name" placeholder="booking_ref">
191
+ <mat-hint>Name to store the extracted value</mat-hint>
192
+ @if (mapping.get('variable_name')?.hasError('pattern')) {
193
+ <mat-error>Lowercase letters, numbers and underscore only</mat-error>
194
+ }
195
+ </mat-form-field>
196
+
197
+ <mat-form-field appearance="outline">
198
+ <mat-label>Caption</mat-label>
199
+ <input matInput formControlName="caption" placeholder="Booking Reference">
200
+ <mat-hint>Human-readable description</mat-hint>
201
+ </mat-form-field>
202
+
203
+ <div class="row">
204
+ <mat-form-field appearance="outline" class="type-field">
205
+ <mat-label>Type</mat-label>
206
+ <mat-select formControlName="type">
207
+ @for (type of variableTypes; track type) {
208
+ <mat-option [value]="type">{{ type }}</mat-option>
209
+ }
210
+ </mat-select>
211
+ </mat-form-field>
212
+
213
+ <mat-form-field appearance="outline" class="path-field">
214
+ <mat-label>JSON Path</mat-label>
215
+ <input matInput formControlName="json_path" placeholder="$.data.bookingReference">
216
+ <mat-hint>JSONPath expression to extract value</mat-hint>
217
+ </mat-form-field>
218
+ </div>
219
+
220
+ <button mat-button color="warn" (click)="removeResponseMapping(i)">
221
+ <mat-icon>delete</mat-icon>
222
+ Remove Mapping
223
+ </button>
224
+ </div>
225
+ </mat-expansion-panel>
226
+ }
227
+ </div>
228
+
229
+ <!-- Retry Settings -->
230
+ <mat-divider></mat-divider>
231
+
232
+ <div class="retry-section" [formGroup]="$any(form.get('retry'))">
233
+ <h3>Retry Settings</h3>
234
+
235
+ <div class="row">
236
+ <mat-form-field appearance="outline">
237
+ <mat-label>Retry Count</mat-label>
238
+ <input matInput type="number" formControlName="retry_count">
239
+ <mat-hint>Number of retry attempts</mat-hint>
240
+ </mat-form-field>
241
+
242
+ <mat-form-field appearance="outline">
243
+ <mat-label>Backoff (seconds)</mat-label>
244
+ <input matInput type="number" formControlName="backoff_seconds">
245
+ <mat-hint>Delay between retries</mat-hint>
246
+ </mat-form-field>
247
+
248
+ <mat-form-field appearance="outline">
249
+ <mat-label>Strategy</mat-label>
250
+ <mat-select formControlName="strategy">
251
+ @for (strategy of retryStrategies; track strategy) {
252
+ <mat-option [value]="strategy">{{ strategy }}</mat-option>
253
+ }
254
+ </mat-select>
255
+ </mat-form-field>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ </mat-tab>
260
+
261
+ <!-- Test Tab -->
262
+ <mat-tab label="Test">
263
+ <div class="tab-content">
264
+ <div class="test-section">
265
+ <h3>Test API Call</h3>
266
+
267
+ <div class="test-controls">
268
+ <button mat-raised-button color="primary"
269
+ (click)="testAPI()"
270
+ [disabled]="testing || !form.get('url')?.valid || !form.get('method')?.valid">
271
+ @if (testing) {
272
+ <ng-container>
273
+ <mat-icon class="spin">sync</mat-icon>
274
+ Testing...
275
+ </ng-container>
276
+ } @else {
277
+ <ng-container>
278
+ <mat-icon>play_arrow</mat-icon>
279
+ Test API
280
+ </ng-container>
281
+ }
282
+ </button>
283
+
284
+ <button mat-button (click)="updateTestRequestJson()">
285
+ <mat-icon>refresh</mat-icon>
286
+ Generate Test Data
287
+ </button>
288
+ </div>
289
+
290
+ <mat-form-field appearance="outline" class="full-width">
291
+ <mat-label>Test Request Body</mat-label>
292
+ <textarea matInput
293
+ [(ngModel)]="testRequestJson"
294
+ rows="10"
295
+ placeholder="Enter test request JSON here"></textarea>
296
+ <mat-hint>Variables will be replaced with test values</mat-hint>
297
+ </mat-form-field>
298
+
299
+ @if (testResult) {
300
+ <mat-divider></mat-divider>
301
+
302
+ <div class="test-result" [class.success]="testResult.success" [class.error]="!testResult.success">
303
+ <h4>Test Result</h4>
304
+
305
+ @if (testResult.success) {
306
+ <div class="result-status">
307
+ <mat-icon>check_circle</mat-icon>
308
+ <span>Success ({{ testResult.status_code }})</span>
309
+ </div>
310
+ } @else {
311
+ <div class="result-status">
312
+ <mat-icon>error</mat-icon>
313
+ <span>Failed: {{ testResult.error }}</span>
314
+ </div>
315
+ }
316
+
317
+ @if (testResult.response_time) {
318
+ <p><strong>Response Time:</strong> {{ testResult.response_time }}ms</p>
319
+ }
320
+
321
+ @if (testResult.response_body) {
322
+ <mat-form-field appearance="outline" class="full-width">
323
+ <mat-label>Response Body</mat-label>
324
+ <textarea matInput
325
+ [value]="testResult.response_body | json"
326
+ rows="10"
327
+ readonly></textarea>
328
+ </mat-form-field>
329
+ }
330
+
331
+ @if (testResult.request_body) {
332
+ <mat-form-field appearance="outline" class="full-width">
333
+ <mat-label>Actual Request Sent</mat-label>
334
+ <textarea matInput
335
+ [value]="testResult.request_body | json"
336
+ rows="8"
337
+ readonly></textarea>
338
+ </mat-form-field>
339
+ }
340
+ </div>
341
+ }
342
+ </div>
343
+ </div>
344
+ </mat-tab>
345
+
346
+ <!-- Auth Tab -->
347
+ <mat-tab label="Authentication">
348
+ <div class="tab-content" [formGroup]="$any(form.get('auth'))">
349
+ <mat-checkbox formControlName="enabled">
350
+ Enable Authentication
351
+ </mat-checkbox>
352
+
353
+ @if (form.get('auth.enabled')?.value) {
354
+ <mat-divider></mat-divider>
355
+
356
+ <div class="auth-section">
357
+ <h3>Token Configuration</h3>
358
+
359
+ <mat-form-field appearance="outline">
360
+ <mat-label>Token Endpoint</mat-label>
361
+ <input matInput formControlName="token_endpoint" placeholder="https://api.example.com/auth/token">
362
+ <mat-hint>URL to obtain authentication token</mat-hint>
363
+ @if (form.get('auth.token_endpoint')?.hasError('required') && form.get('auth.token_endpoint')?.touched) {
364
+ <mat-error>Token endpoint is required when auth is enabled</mat-error>
365
+ }
366
+ </mat-form-field>
367
+
368
+ <mat-form-field appearance="outline">
369
+ <mat-label>Token Response Path</mat-label>
370
+ <input matInput formControlName="response_token_path" placeholder="token">
371
+ <mat-hint>JSON path to extract token from response</mat-hint>
372
+ @if (form.get('auth.response_token_path')?.hasError('required') && form.get('auth.response_token_path')?.touched) {
373
+ <mat-error>Token path is required when auth is enabled</mat-error>
374
+ }
375
+ </mat-form-field>
376
+
377
+ <mat-form-field appearance="outline" class="full-width">
378
+ <mat-label>Token Request Body</mat-label>
379
+ <textarea matInput
380
+ formControlName="token_request_body"
381
+ rows="6"
382
+ (click)="saveCursorPosition('auth.token_request_body', $event)"
383
+ (keyup)="saveCursorPosition('auth.token_request_body', $event)"
384
+ placeholder='{"username": "api_user", "password": "api_pass"}'></textarea>
385
+ <mat-hint>JSON body for token request</mat-hint>
386
+ @if (!validateJSON('auth.token_request_body')) {
387
+ <mat-error>Invalid JSON format</mat-error>
388
+ }
389
+ </mat-form-field>
390
+
391
+ <div class="template-variables">
392
+ <strong>Available Variables:</strong>
393
+ <mat-chip-set>
394
+ @for (variable of getTemplateVariables(); track variable) {
395
+ <mat-chip (click)="insertTemplateVariable('auth.token_request_body', variable)">
396
+ {{ variable }}
397
+ </mat-chip>
398
+ }
399
+ </mat-chip-set>
400
+ </div>
401
+
402
+ <mat-divider></mat-divider>
403
+
404
+ <h3>Token Refresh (Optional)</h3>
405
+
406
+ <mat-form-field appearance="outline">
407
+ <mat-label>Refresh Endpoint</mat-label>
408
+ <input matInput formControlName="token_refresh_endpoint" placeholder="https://api.example.com/auth/refresh">
409
+ <mat-hint>URL to refresh expired token</mat-hint>
410
+ </mat-form-field>
411
+
412
+ <mat-form-field appearance="outline" class="full-width">
413
+ <mat-label>Refresh Request Body</mat-label>
414
+ <textarea matInput
415
+ formControlName="token_refresh_body"
416
+ rows="4"
417
+ (click)="saveCursorPosition('auth.token_refresh_body', $event)"
418
+ (keyup)="saveCursorPosition('auth.token_refresh_body', $event)"
419
+ placeholder='{"refresh_token": "your_refresh_token"}'></textarea>
420
+ <mat-hint>JSON body for refresh request</mat-hint>
421
+ @if (!validateJSON('auth.token_refresh_body')) {
422
+ <mat-error>Invalid JSON format</mat-error>
423
+ }
424
+ </mat-form-field>
425
+
426
+ <div class="template-variables">
427
+ <strong>Available Variables:</strong>
428
+ <mat-chip-set>
429
+ @for (variable of getTemplateVariables(); track variable) {
430
+ <mat-chip (click)="insertTemplateVariable('auth.token_refresh_body', variable)">
431
+ {{ variable }}
432
+ </mat-chip>
433
+ }
434
+ </mat-chip-set>
435
+ </div>
436
+ </div>
437
+ }
438
+ </div>
439
+ </mat-tab>
440
+ </mat-tab-group>
441
+ </mat-dialog-content>
442
+
443
+ <mat-dialog-actions align="end">
444
+ <button mat-button (click)="cancel()">
445
+ @if (data.mode === 'test') {
446
+ Close
447
+ } @else {
448
+ Cancel
449
+ }
450
+ </button>
451
+ @if (data.mode !== 'test') {
452
+ <button mat-raised-button color="primary"
453
+ (click)="save()"
454
+ [disabled]="saving || form.invalid">
455
+ @if (saving) {
456
+ <ng-container>
457
+ <mat-icon class="spin">sync</mat-icon>
458
+ Saving...
459
+ </ng-container>
460
+ } @else {
461
+ @if (data.mode === 'create' || data.mode === 'duplicate') {
462
+ Create
463
+ } @else {
464
+ Update
465
+ }
466
+ }
467
+ </button>
468
+ }
469
  </mat-dialog-actions>
flare-ui/src/app/dialogs/api-edit-dialog/api-edit-dialog.component.scss CHANGED
@@ -1,327 +1,327 @@
1
- .tab-content {
2
- padding: 24px;
3
- overflow-y: auto;
4
- max-height: calc(80vh - 200px);
5
- }
6
-
7
- .section-title {
8
- font-size: 16px;
9
- font-weight: 500;
10
- margin: 24px 0 16px 0;
11
- color: #666;
12
-
13
- &:first-child {
14
- margin-top: 0;
15
- }
16
- }
17
-
18
- .section-header {
19
- display: flex;
20
- justify-content: space-between;
21
- align-items: center;
22
- margin-bottom: 16px;
23
-
24
- h4 {
25
- margin: 0;
26
- font-size: 16px;
27
- font-weight: 500;
28
- }
29
- }
30
-
31
- .form-section {
32
- margin-bottom: 24px;
33
- }
34
-
35
- .section-label {
36
- display: block;
37
- margin-bottom: 8px;
38
- font-size: 14px;
39
- font-weight: 500;
40
- color: rgba(0, 0, 0, 0.87);
41
- }
42
-
43
- .full-width {
44
- width: 100%;
45
- }
46
-
47
- .headers-list {
48
- .header-row {
49
- display: flex;
50
- gap: 16px;
51
- align-items: flex-start;
52
- margin-bottom: 16px;
53
-
54
- mat-form-field {
55
- flex: 1;
56
- }
57
-
58
- .header-value {
59
- flex: 2;
60
- }
61
-
62
- button {
63
- margin-top: 8px;
64
- }
65
- }
66
- }
67
-
68
- .response-mappings-list {
69
- .mapping-row {
70
- display: flex;
71
- gap: 16px;
72
- align-items: flex-start;
73
- margin-bottom: 16px;
74
-
75
- mat-form-field {
76
- flex: 1;
77
- }
78
-
79
- .json-path-field {
80
- flex: 2;
81
- }
82
-
83
- button {
84
- margin-top: 8px;
85
- }
86
- }
87
- }
88
-
89
- .empty-state {
90
- text-align: center;
91
- padding: 24px;
92
- background-color: #f5f5f5;
93
- border-radius: 4px;
94
- margin: 16px 0;
95
-
96
- mat-icon {
97
- font-size: 48px;
98
- width: 48px;
99
- height: 48px;
100
- color: #ccc;
101
- }
102
-
103
- p {
104
- margin-top: 16px;
105
- color: #666;
106
- }
107
- }
108
-
109
- .empty-state-small {
110
- text-align: center;
111
- padding: 16px;
112
- background-color: #f5f5f5;
113
- border-radius: 4px;
114
- margin: 16px 0;
115
-
116
- mat-icon {
117
- font-size: 24px;
118
- width: 24px;
119
- height: 24px;
120
- color: #ccc;
121
- }
122
-
123
- p {
124
- margin-top: 8px;
125
- font-size: 14px;
126
- color: #666;
127
- }
128
- }
129
-
130
- .auth-settings {
131
- margin-top: 16px;
132
- }
133
-
134
- .retry-settings {
135
- display: flex;
136
- gap: 16px;
137
- align-items: flex-start;
138
-
139
- mat-form-field {
140
- flex: 1;
141
- }
142
- }
143
-
144
- .template-helpers {
145
- margin-top: 8px;
146
-
147
- .template-list {
148
- display: flex;
149
- flex-wrap: wrap;
150
- gap: 8px;
151
- padding: 16px;
152
- }
153
-
154
- .template-chip {
155
- cursor: pointer;
156
- transition: all 0.2s;
157
-
158
- &:hover {
159
- transform: scale(1.05);
160
- box-shadow: 0 2px 4px rgba(0,0,0,0.2);
161
- }
162
- }
163
-
164
- .hint-text {
165
- padding: 16px;
166
- color: #666;
167
- font-size: 14px;
168
- line-height: 1.5;
169
- }
170
- }
171
-
172
- .json-editor {
173
- position: relative;
174
-
175
- .json-textarea {
176
- width: 100%;
177
- font-family: 'Monaco', 'Consolas', monospace;
178
- font-size: 13px;
179
- padding: 12px;
180
- border: 1px solid #ccc;
181
- border-radius: 4px;
182
- background-color: #f8f8f8;
183
- resize: vertical;
184
- min-height: 120px;
185
-
186
- &:focus {
187
- outline: none;
188
- border-color: #3f51b5;
189
- background-color: #fff;
190
- }
191
- }
192
-
193
- button {
194
- margin-top: 8px;
195
- }
196
- }
197
-
198
- .test-section {
199
- .test-info {
200
- margin-bottom: 24px;
201
- }
202
-
203
- .test-result {
204
- margin-top: 24px;
205
-
206
- .result-status {
207
- display: flex;
208
- align-items: center;
209
- gap: 12px;
210
- padding: 16px;
211
- border-radius: 4px;
212
- margin-bottom: 16px;
213
-
214
- &.success {
215
- background-color: #e8f5e9;
216
- color: #2e7d32;
217
- }
218
-
219
- &.error {
220
- background-color: #ffebee;
221
- color: #c62828;
222
- }
223
-
224
- mat-icon {
225
- font-size: 28px;
226
- width: 28px;
227
- height: 28px;
228
- }
229
-
230
- .status-code,
231
- .response-time {
232
- margin-left: auto;
233
- font-size: 14px;
234
- opacity: 0.8;
235
- }
236
- }
237
-
238
- .error-message {
239
- display: flex;
240
- align-items: center;
241
- gap: 8px;
242
- padding: 12px;
243
- background-color: #ffebee;
244
- color: #c62828;
245
- border-radius: 4px;
246
- margin-bottom: 16px;
247
- }
248
-
249
- .response-section {
250
- margin-bottom: 16px;
251
-
252
- h5 {
253
- margin-bottom: 8px;
254
- font-size: 14px;
255
- font-weight: 500;
256
- }
257
-
258
- pre {
259
- padding: 12px;
260
- background-color: #f5f5f5;
261
- border-radius: 4px;
262
- overflow-x: auto;
263
- font-size: 12px;
264
- margin: 0;
265
- white-space: pre-wrap;
266
- word-break: break-word;
267
- }
268
- }
269
- }
270
-
271
- .test-note {
272
- display: flex;
273
- align-items: flex-start;
274
- gap: 8px;
275
- padding: 12px;
276
- background-color: #e3f2fd;
277
- border-radius: 4px;
278
- margin-top: 16px;
279
-
280
- mat-icon {
281
- color: #1976d2;
282
- font-size: 20px;
283
- width: 20px;
284
- height: 20px;
285
- }
286
-
287
- p {
288
- margin: 0;
289
- font-size: 14px;
290
- color: #1976d2;
291
- }
292
- }
293
- }
294
-
295
- mat-dialog-actions {
296
- padding: 16px 24px;
297
- margin: 0;
298
- border-top: 1px solid #e0e0e0;
299
- }
300
-
301
- .mat-mdc-form-field {
302
- margin-bottom: 16px;
303
- }
304
-
305
- .mat-mdc-dialog-content {
306
- padding: 0;
307
- overflow: hidden;
308
- min-height: 500px;
309
- max-height: 80vh;
310
- display: flex;
311
- flex-direction: column;
312
- }
313
-
314
- .mat-mdc-tab-group {
315
- flex: 1;
316
- display: flex;
317
- flex-direction: column;
318
- }
319
-
320
- .mat-mdc-tab-body-wrapper {
321
- flex: 1;
322
- overflow: auto;
323
- }
324
-
325
- .mat-mdc-chip-set .mat-mdc-chip {
326
- margin: 0 4px 4px 0 !important;
327
  }
 
1
+ .tab-content {
2
+ padding: 24px;
3
+ overflow-y: auto;
4
+ max-height: calc(80vh - 200px);
5
+ }
6
+
7
+ .section-title {
8
+ font-size: 16px;
9
+ font-weight: 500;
10
+ margin: 24px 0 16px 0;
11
+ color: #666;
12
+
13
+ &:first-child {
14
+ margin-top: 0;
15
+ }
16
+ }
17
+
18
+ .section-header {
19
+ display: flex;
20
+ justify-content: space-between;
21
+ align-items: center;
22
+ margin-bottom: 16px;
23
+
24
+ h4 {
25
+ margin: 0;
26
+ font-size: 16px;
27
+ font-weight: 500;
28
+ }
29
+ }
30
+
31
+ .form-section {
32
+ margin-bottom: 24px;
33
+ }
34
+
35
+ .section-label {
36
+ display: block;
37
+ margin-bottom: 8px;
38
+ font-size: 14px;
39
+ font-weight: 500;
40
+ color: rgba(0, 0, 0, 0.87);
41
+ }
42
+
43
+ .full-width {
44
+ width: 100%;
45
+ }
46
+
47
+ .headers-list {
48
+ .header-row {
49
+ display: flex;
50
+ gap: 16px;
51
+ align-items: flex-start;
52
+ margin-bottom: 16px;
53
+
54
+ mat-form-field {
55
+ flex: 1;
56
+ }
57
+
58
+ .header-value {
59
+ flex: 2;
60
+ }
61
+
62
+ button {
63
+ margin-top: 8px;
64
+ }
65
+ }
66
+ }
67
+
68
+ .response-mappings-list {
69
+ .mapping-row {
70
+ display: flex;
71
+ gap: 16px;
72
+ align-items: flex-start;
73
+ margin-bottom: 16px;
74
+
75
+ mat-form-field {
76
+ flex: 1;
77
+ }
78
+
79
+ .json-path-field {
80
+ flex: 2;
81
+ }
82
+
83
+ button {
84
+ margin-top: 8px;
85
+ }
86
+ }
87
+ }
88
+
89
+ .empty-state {
90
+ text-align: center;
91
+ padding: 24px;
92
+ background-color: #f5f5f5;
93
+ border-radius: 4px;
94
+ margin: 16px 0;
95
+
96
+ mat-icon {
97
+ font-size: 48px;
98
+ width: 48px;
99
+ height: 48px;
100
+ color: #ccc;
101
+ }
102
+
103
+ p {
104
+ margin-top: 16px;
105
+ color: #666;
106
+ }
107
+ }
108
+
109
+ .empty-state-small {
110
+ text-align: center;
111
+ padding: 16px;
112
+ background-color: #f5f5f5;
113
+ border-radius: 4px;
114
+ margin: 16px 0;
115
+
116
+ mat-icon {
117
+ font-size: 24px;
118
+ width: 24px;
119
+ height: 24px;
120
+ color: #ccc;
121
+ }
122
+
123
+ p {
124
+ margin-top: 8px;
125
+ font-size: 14px;
126
+ color: #666;
127
+ }
128
+ }
129
+
130
+ .auth-settings {
131
+ margin-top: 16px;
132
+ }
133
+
134
+ .retry-settings {
135
+ display: flex;
136
+ gap: 16px;
137
+ align-items: flex-start;
138
+
139
+ mat-form-field {
140
+ flex: 1;
141
+ }
142
+ }
143
+
144
+ .template-helpers {
145
+ margin-top: 8px;
146
+
147
+ .template-list {
148
+ display: flex;
149
+ flex-wrap: wrap;
150
+ gap: 8px;
151
+ padding: 16px;
152
+ }
153
+
154
+ .template-chip {
155
+ cursor: pointer;
156
+ transition: all 0.2s;
157
+
158
+ &:hover {
159
+ transform: scale(1.05);
160
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
161
+ }
162
+ }
163
+
164
+ .hint-text {
165
+ padding: 16px;
166
+ color: #666;
167
+ font-size: 14px;
168
+ line-height: 1.5;
169
+ }
170
+ }
171
+
172
+ .json-editor {
173
+ position: relative;
174
+
175
+ .json-textarea {
176
+ width: 100%;
177
+ font-family: 'Monaco', 'Consolas', monospace;
178
+ font-size: 13px;
179
+ padding: 12px;
180
+ border: 1px solid #ccc;
181
+ border-radius: 4px;
182
+ background-color: #f8f8f8;
183
+ resize: vertical;
184
+ min-height: 120px;
185
+
186
+ &:focus {
187
+ outline: none;
188
+ border-color: #3f51b5;
189
+ background-color: #fff;
190
+ }
191
+ }
192
+
193
+ button {
194
+ margin-top: 8px;
195
+ }
196
+ }
197
+
198
+ .test-section {
199
+ .test-info {
200
+ margin-bottom: 24px;
201
+ }
202
+
203
+ .test-result {
204
+ margin-top: 24px;
205
+
206
+ .result-status {
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 12px;
210
+ padding: 16px;
211
+ border-radius: 4px;
212
+ margin-bottom: 16px;
213
+
214
+ &.success {
215
+ background-color: #e8f5e9;
216
+ color: #2e7d32;
217
+ }
218
+
219
+ &.error {
220
+ background-color: #ffebee;
221
+ color: #c62828;
222
+ }
223
+
224
+ mat-icon {
225
+ font-size: 28px;
226
+ width: 28px;
227
+ height: 28px;
228
+ }
229
+
230
+ .status-code,
231
+ .response-time {
232
+ margin-left: auto;
233
+ font-size: 14px;
234
+ opacity: 0.8;
235
+ }
236
+ }
237
+
238
+ .error-message {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 8px;
242
+ padding: 12px;
243
+ background-color: #ffebee;
244
+ color: #c62828;
245
+ border-radius: 4px;
246
+ margin-bottom: 16px;
247
+ }
248
+
249
+ .response-section {
250
+ margin-bottom: 16px;
251
+
252
+ h5 {
253
+ margin-bottom: 8px;
254
+ font-size: 14px;
255
+ font-weight: 500;
256
+ }
257
+
258
+ pre {
259
+ padding: 12px;
260
+ background-color: #f5f5f5;
261
+ border-radius: 4px;
262
+ overflow-x: auto;
263
+ font-size: 12px;
264
+ margin: 0;
265
+ white-space: pre-wrap;
266
+ word-break: break-word;
267
+ }
268
+ }
269
+ }
270
+
271
+ .test-note {
272
+ display: flex;
273
+ align-items: flex-start;
274
+ gap: 8px;
275
+ padding: 12px;
276
+ background-color: #e3f2fd;
277
+ border-radius: 4px;
278
+ margin-top: 16px;
279
+
280
+ mat-icon {
281
+ color: #1976d2;
282
+ font-size: 20px;
283
+ width: 20px;
284
+ height: 20px;
285
+ }
286
+
287
+ p {
288
+ margin: 0;
289
+ font-size: 14px;
290
+ color: #1976d2;
291
+ }
292
+ }
293
+ }
294
+
295
+ mat-dialog-actions {
296
+ padding: 16px 24px;
297
+ margin: 0;
298
+ border-top: 1px solid #e0e0e0;
299
+ }
300
+
301
+ .mat-mdc-form-field {
302
+ margin-bottom: 16px;
303
+ }
304
+
305
+ .mat-mdc-dialog-content {
306
+ padding: 0;
307
+ overflow: hidden;
308
+ min-height: 500px;
309
+ max-height: 80vh;
310
+ display: flex;
311
+ flex-direction: column;
312
+ }
313
+
314
+ .mat-mdc-tab-group {
315
+ flex: 1;
316
+ display: flex;
317
+ flex-direction: column;
318
+ }
319
+
320
+ .mat-mdc-tab-body-wrapper {
321
+ flex: 1;
322
+ overflow: auto;
323
+ }
324
+
325
+ .mat-mdc-chip-set .mat-mdc-chip {
326
+ margin: 0 4px 4px 0 !important;
327
  }
flare-ui/src/app/dialogs/api-edit-dialog/api-edit-dialog.component.ts CHANGED
@@ -1,631 +1,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
-
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
+
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
  }