@bestcodetools/graphql-playground 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +8 -0
- package/.github/workflows/docker-publish.yml +48 -0
- package/.github/workflows/npm-publish.yml +22 -0
- package/Dockerfile +28 -0
- package/README.md +110 -0
- package/jest.config.js +8 -0
- package/package.json +49 -0
- package/public/components/config-editor.html +38 -0
- package/public/components/field-args.html +16 -0
- package/public/components/field-type.html +27 -0
- package/public/components/headers-editor.html +3 -0
- package/public/components/query-editor.html +3 -0
- package/public/components/response-viewer.html +3 -0
- package/public/components/schema-viewer.html +74 -0
- package/public/components/variables-editor.html +3 -0
- package/public/index.html +114 -0
- package/public/js/components/config-editor.js +122 -0
- package/public/js/components/headers-editor.js +562 -0
- package/public/js/components/query-editor.js +1896 -0
- package/public/js/components/response-viewer.js +201 -0
- package/public/js/components/schema-viewer.js +644 -0
- package/public/js/components/variables-editor.js +1258 -0
- package/public/js/main.js +1016 -0
- package/public/styles/main.css +1313 -0
- package/public/styles/main.css.map +1 -0
- package/public/styles/main.scss +1319 -0
- package/src/middleware.ts +36 -0
- package/src/standalone.ts +14 -0
- package/tests/standalone.test.ts +74 -0
- package/tsconfig.json +71 -0
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
var app = angular.module('app', []);
|
|
2
|
+
|
|
3
|
+
app.factory('AppState', function () {
|
|
4
|
+
const tryParse = (value) => {
|
|
5
|
+
if(value === null) return null;
|
|
6
|
+
try { return JSON.parse(value); }
|
|
7
|
+
catch (e) { return value; }
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
saveKey: function (key, value) {
|
|
11
|
+
sessionStorage.setItem(key, angular.$$stringify(value));
|
|
12
|
+
},
|
|
13
|
+
getKey: function (key) {
|
|
14
|
+
const value = sessionStorage.getItem(key);
|
|
15
|
+
return tryParse(value);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
app.factory('I18nService', ['AppState', function (AppState) {
|
|
21
|
+
const STORAGE_KEY = 'locale';
|
|
22
|
+
const dictionaries = {
|
|
23
|
+
en: {
|
|
24
|
+
'app.name': 'GraphQL Playground',
|
|
25
|
+
'app.developed_by': 'Developed by BestCodeTools',
|
|
26
|
+
'nav.settings': 'Settings',
|
|
27
|
+
'nav.language': 'Language',
|
|
28
|
+
'language.en': 'English',
|
|
29
|
+
'language.pt-BR': 'Portuguese (Brazil)',
|
|
30
|
+
'actions.send': 'Send',
|
|
31
|
+
'actions.close': 'Close',
|
|
32
|
+
'actions.format': 'Format',
|
|
33
|
+
'actions.new_query': 'New Query',
|
|
34
|
+
'actions.new_mutation': 'New Mutation',
|
|
35
|
+
'modal.settings': 'Settings',
|
|
36
|
+
'tabs.default': 'Tab',
|
|
37
|
+
'editor.query': 'Query Editor',
|
|
38
|
+
'editor.response': 'Response Body',
|
|
39
|
+
'editor.variables': 'Variables Payload',
|
|
40
|
+
'editor.headers': 'Request Headers',
|
|
41
|
+
'schema.title': 'Schema Viewer',
|
|
42
|
+
'schema.search_placeholder': 'Search types or first-level fields',
|
|
43
|
+
'schema.note': 'Ctrl+Click a root query, mutation, or subscription field to add it to the active payload with operation variables.',
|
|
44
|
+
'schema.error': 'An error occurred while loading the schema',
|
|
45
|
+
'schema.retry': 'Retry to load schema',
|
|
46
|
+
'config.shared_headers': 'Shared Headers',
|
|
47
|
+
'config.other': 'Other',
|
|
48
|
+
'config.property': 'Property',
|
|
49
|
+
'config.value': 'Value',
|
|
50
|
+
'config.key_placeholder': 'Key',
|
|
51
|
+
'config.value_placeholder': 'Value',
|
|
52
|
+
'config.add_header': '+ Add Header',
|
|
53
|
+
'config.other_placeholder': 'Other',
|
|
54
|
+
'workspace.export': 'Export Workspace',
|
|
55
|
+
'workspace.import': 'Import Workspace',
|
|
56
|
+
'workspace.note': 'Export and import your full workspace, including tabs and settings.',
|
|
57
|
+
'workspace.import_error': 'Could not import this workspace file.',
|
|
58
|
+
'workspace.token_placeholder': '[your token here]',
|
|
59
|
+
'workspace.key_placeholder': '[your key here]',
|
|
60
|
+
'query.placeholder': '/* Type your query here */'
|
|
61
|
+
},
|
|
62
|
+
'pt-BR': {
|
|
63
|
+
'app.name': 'GraphQL Playground',
|
|
64
|
+
'app.developed_by': 'Desenvolvido por BestCodeTools',
|
|
65
|
+
'nav.settings': 'Configurações',
|
|
66
|
+
'nav.language': 'Idioma',
|
|
67
|
+
'language.en': 'English',
|
|
68
|
+
'language.pt-BR': 'Português (Brasil)',
|
|
69
|
+
'actions.send': 'Enviar',
|
|
70
|
+
'actions.close': 'Fechar',
|
|
71
|
+
'actions.format': 'Formatar',
|
|
72
|
+
'actions.new_query': 'Nova Query',
|
|
73
|
+
'actions.new_mutation': 'Nova Mutation',
|
|
74
|
+
'modal.settings': 'Configurações',
|
|
75
|
+
'tabs.default': 'Aba',
|
|
76
|
+
'editor.query': 'Editor de Query',
|
|
77
|
+
'editor.response': 'Response Body',
|
|
78
|
+
'editor.variables': 'Payload de Variables',
|
|
79
|
+
'editor.headers': 'Request Headers',
|
|
80
|
+
'schema.title': 'Schema Viewer',
|
|
81
|
+
'schema.search_placeholder': 'Buscar types ou fields de primeiro nível',
|
|
82
|
+
'schema.note': 'Ctrl+Click em um field raiz de query, mutation ou subscription para adicionar ao payload ativo com operation variables.',
|
|
83
|
+
'schema.error': 'Ocorreu um erro ao carregar o schema',
|
|
84
|
+
'schema.retry': 'Tentar carregar o schema novamente',
|
|
85
|
+
'config.shared_headers': 'Shared Headers',
|
|
86
|
+
'config.other': 'Outros',
|
|
87
|
+
'config.property': 'Propriedade',
|
|
88
|
+
'config.value': 'Valor',
|
|
89
|
+
'config.key_placeholder': 'Chave',
|
|
90
|
+
'config.value_placeholder': 'Valor',
|
|
91
|
+
'config.add_header': '+ Adicionar Header',
|
|
92
|
+
'config.other_placeholder': 'Outras Configurações (placeholder)',
|
|
93
|
+
'query.placeholder': '/* Digite sua query aqui */'
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
let locale = AppState.getKey(STORAGE_KEY) || 'pt-BR';
|
|
98
|
+
if (!dictionaries[locale]) {
|
|
99
|
+
locale = 'pt-BR';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
getLocale: function () {
|
|
104
|
+
return locale;
|
|
105
|
+
},
|
|
106
|
+
setLocale: function (nextLocale) {
|
|
107
|
+
if (!dictionaries[nextLocale]) {
|
|
108
|
+
return locale;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
locale = nextLocale;
|
|
112
|
+
AppState.saveKey(STORAGE_KEY, locale);
|
|
113
|
+
return locale;
|
|
114
|
+
},
|
|
115
|
+
getLocales: function () {
|
|
116
|
+
return [
|
|
117
|
+
{ code: 'en', labelKey: 'language.en' },
|
|
118
|
+
{ code: 'pt-BR', labelKey: 'language.pt-BR' }
|
|
119
|
+
];
|
|
120
|
+
},
|
|
121
|
+
t: function (key) {
|
|
122
|
+
return (dictionaries[locale] && dictionaries[locale][key])
|
|
123
|
+
|| (dictionaries.en && dictionaries.en[key])
|
|
124
|
+
|| key;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}]);
|
|
128
|
+
// Serviço para gerenciar o estado das abas
|
|
129
|
+
app.factory('TabService', ['AppState', function (state) {
|
|
130
|
+
const STORAGE_KEY = 'tabs';
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
getTabs: function () {
|
|
134
|
+
const tabs = state.getKey(STORAGE_KEY);
|
|
135
|
+
return tabs ? tabs : [];
|
|
136
|
+
},
|
|
137
|
+
saveTabs: function (tabs) {
|
|
138
|
+
state.saveKey(STORAGE_KEY, tabs);
|
|
139
|
+
},
|
|
140
|
+
generateTabId: function () {
|
|
141
|
+
return Date.now() + Math.random().toString(36).substr(2, 5);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}]);
|
|
145
|
+
|
|
146
|
+
app.controller('MainController', ['$scope', '$timeout', 'AppState', 'TabService', 'I18nService', function ($scope, $timeout, AppState, TabService, I18nService) {
|
|
147
|
+
const $ctrl = this;
|
|
148
|
+
const ACTIVE_TAB_STORAGE_KEY = 'activeTab';
|
|
149
|
+
const SHARED_HEADERS_STORAGE_KEY = 'sharedHeaders';
|
|
150
|
+
$scope.$ctrl = $ctrl;
|
|
151
|
+
// Estado inicial
|
|
152
|
+
$ctrl.title = 'AngularJS Tutorial Example';
|
|
153
|
+
$ctrl.message = 'Hello World!';
|
|
154
|
+
$ctrl.ready = true;
|
|
155
|
+
$ctrl.appVersion = 'v1.0.2';
|
|
156
|
+
$ctrl.locales = I18nService.getLocales();
|
|
157
|
+
$ctrl.locale = I18nService.getLocale();
|
|
158
|
+
$ctrl.t = function (key) {
|
|
159
|
+
return I18nService.t(key);
|
|
160
|
+
};
|
|
161
|
+
$ctrl.tabs = [];
|
|
162
|
+
$ctrl.activeTab = 0;
|
|
163
|
+
$ctrl.schema = null;
|
|
164
|
+
$ctrl.loadSchemaError = null;
|
|
165
|
+
function getDefaultGraphqlUrl() {
|
|
166
|
+
if (typeof window === 'undefined' || !window.location) {
|
|
167
|
+
return 'http://localhost:4000/graphql';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { protocol, hostname, host } = window.location;
|
|
171
|
+
const normalizedHost = String(hostname || '').toLowerCase();
|
|
172
|
+
const isLoopbackHost = normalizedHost === 'localhost'
|
|
173
|
+
|| normalizedHost === '127.0.0.1'
|
|
174
|
+
|| normalizedHost === '::1'
|
|
175
|
+
|| normalizedHost === '[::1]';
|
|
176
|
+
|
|
177
|
+
if (isLoopbackHost) {
|
|
178
|
+
return 'http://localhost:4000/graphql';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return `${protocol}//${host}/graphql`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
$ctrl.url = sessionStorage.getItem('url') || getDefaultGraphqlUrl();
|
|
185
|
+
// Estado inicial
|
|
186
|
+
$ctrl.showConfig = false;
|
|
187
|
+
|
|
188
|
+
// Abrir e fechar modal
|
|
189
|
+
$ctrl.openConfig = function () {
|
|
190
|
+
$ctrl.showConfig = true;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
$ctrl.closeConfig = function () {
|
|
194
|
+
$ctrl.showConfig = false;
|
|
195
|
+
};
|
|
196
|
+
$ctrl.setLocale = function (locale) {
|
|
197
|
+
$ctrl.locale = I18nService.setLocale(locale);
|
|
198
|
+
};
|
|
199
|
+
function debounce(func, wait) {
|
|
200
|
+
let timeout;
|
|
201
|
+
return function (...args) {
|
|
202
|
+
clearTimeout(timeout);
|
|
203
|
+
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const persistTabsDebounced = debounce(() => {
|
|
207
|
+
TabService.saveTabs($ctrl.tabs);
|
|
208
|
+
}, 250);
|
|
209
|
+
$ctrl.persistTabs = function () {
|
|
210
|
+
persistTabsDebounced();
|
|
211
|
+
sessionStorage.setItem(ACTIVE_TAB_STORAGE_KEY, String($ctrl.activeTab || 0));
|
|
212
|
+
};
|
|
213
|
+
$ctrl.saveTabsToSessionStorage = function () {
|
|
214
|
+
$ctrl.persistTabs();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
function getTabTitleFromQuery(queryText, tabIndex) {
|
|
218
|
+
const safeQuery = typeof queryText === 'string' ? queryText : '';
|
|
219
|
+
const queryWithoutComments = safeQuery
|
|
220
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
221
|
+
.replace(/([^:]|^)\/\/.*$/gm, '$1')
|
|
222
|
+
.replace(/#.*/g, '');
|
|
223
|
+
|
|
224
|
+
const namedOperationMatch = /\b(query|mutation|subscription)\s+([^\(\{\}\)\s]+)/.exec(queryWithoutComments);
|
|
225
|
+
if (namedOperationMatch) {
|
|
226
|
+
return `${namedOperationMatch[1]} ${namedOperationMatch[2]}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const anonymousOperationMatch = /\b(query|mutation|subscription)\b\s*(\([^)]*\))?\s*\{([\s\S]*)\}/.exec(queryWithoutComments);
|
|
230
|
+
if (anonymousOperationMatch) {
|
|
231
|
+
const selectionBody = anonymousOperationMatch[3] || '';
|
|
232
|
+
const firstRootFieldMatch = /(^|[\s{])([A-Za-z_][A-Za-z0-9_]*)(?=\s*(\(|\{|@|$|\n))/m.exec(selectionBody);
|
|
233
|
+
|
|
234
|
+
if (firstRootFieldMatch && firstRootFieldMatch[2]) {
|
|
235
|
+
return firstRootFieldMatch[2];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return `${$ctrl.t('tabs.default')} ${tabIndex}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getWorkspaceLabel(key) {
|
|
243
|
+
const isPtBr = I18nService.getLocale() === 'pt-BR';
|
|
244
|
+
|
|
245
|
+
const labels = {
|
|
246
|
+
export: isPtBr ? 'Exportar Workspace' : 'Export Workspace',
|
|
247
|
+
import: isPtBr ? 'Importar Workspace' : 'Import Workspace',
|
|
248
|
+
note: isPtBr
|
|
249
|
+
? 'Exporte e importe seu workspace completo, incluindo abas e configurações.'
|
|
250
|
+
: 'Export and import your full workspace, including tabs and settings.',
|
|
251
|
+
importError: isPtBr
|
|
252
|
+
? 'Não foi possível importar este arquivo de workspace.'
|
|
253
|
+
: 'Could not import this workspace file.',
|
|
254
|
+
tokenPlaceholder: isPtBr ? '[seu token aqui]' : '[your token here]',
|
|
255
|
+
keyPlaceholder: isPtBr ? '[sua chave aqui]' : '[your key here]'
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return labels[key] || '';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isSensitiveHeaderName(headerName) {
|
|
262
|
+
const normalizedName = String(headerName || '').trim().toLowerCase();
|
|
263
|
+
return normalizedName === 'authorization' || normalizedName.includes('token') || normalizedName.includes('key');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getHeaderExportPlaceholder(headerName) {
|
|
267
|
+
const normalizedName = String(headerName || '').trim().toLowerCase();
|
|
268
|
+
return normalizedName.includes('key')
|
|
269
|
+
? getWorkspaceLabel('keyPlaceholder')
|
|
270
|
+
: getWorkspaceLabel('tokenPlaceholder');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function sanitizeHeadersTextForExport(headersText) {
|
|
274
|
+
const safeText = typeof headersText === 'string' ? headersText : '{}';
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const parsed = JSON.parse(safeText);
|
|
278
|
+
|
|
279
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
280
|
+
return safeText;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sanitized = Object.keys(parsed).reduce((acc, key) => {
|
|
284
|
+
acc[key] = isSensitiveHeaderName(key)
|
|
285
|
+
? getHeaderExportPlaceholder(key)
|
|
286
|
+
: parsed[key];
|
|
287
|
+
return acc;
|
|
288
|
+
}, {});
|
|
289
|
+
|
|
290
|
+
return JSON.stringify(sanitized, null, 2);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
return safeText.replace(/"([^"]+)"\s*:\s*"([^"]*)"/g, (match, key) => {
|
|
293
|
+
if (!isSensitiveHeaderName(key)) {
|
|
294
|
+
return match;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return `"${key}": "${getHeaderExportPlaceholder(key)}"`;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function sanitizeSharedHeadersForExport(headers) {
|
|
303
|
+
if (!Array.isArray(headers)) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return headers.map((header) => {
|
|
308
|
+
const key = header && typeof header.key === 'string' ? header.key : '';
|
|
309
|
+
const value = header && typeof header.value === 'string' ? header.value : '';
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
key,
|
|
313
|
+
value: isSensitiveHeaderName(key) ? getHeaderExportPlaceholder(key) : value
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function parseJsonObject(text, fallbackValue) {
|
|
319
|
+
const safeText = typeof text === 'string' ? text.trim() : '';
|
|
320
|
+
|
|
321
|
+
if (!safeText) {
|
|
322
|
+
return fallbackValue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const parsed = JSON.parse(safeText);
|
|
326
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
327
|
+
throw new Error('Expected a JSON object.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return parsed;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getSharedHeadersMap() {
|
|
334
|
+
const sharedHeaders = AppState.getKey(SHARED_HEADERS_STORAGE_KEY) || [];
|
|
335
|
+
|
|
336
|
+
if (!Array.isArray(sharedHeaders)) {
|
|
337
|
+
return {};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return sharedHeaders.reduce((acc, header) => {
|
|
341
|
+
const key = header && typeof header.key === 'string' ? header.key.trim() : '';
|
|
342
|
+
const value = header && typeof header.value === 'string' ? header.value : '';
|
|
343
|
+
|
|
344
|
+
if (!key) {
|
|
345
|
+
return acc;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
acc[key] = value;
|
|
349
|
+
return acc;
|
|
350
|
+
}, {});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function formatResponseBody(response, payload) {
|
|
354
|
+
const contentType = response.headers.get('content-type') || '';
|
|
355
|
+
const isJsonResponse = contentType.toLowerCase().includes('application/json');
|
|
356
|
+
|
|
357
|
+
return Promise.resolve().then(() => {
|
|
358
|
+
if (isJsonResponse) {
|
|
359
|
+
return response.json().then((data) => JSON.stringify(data, null, 2));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return response.text().then((text) => {
|
|
363
|
+
if (!text) {
|
|
364
|
+
return JSON.stringify(payload, null, 2);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return text;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function applyAutomaticTabTitle(tab, tabIndex) {
|
|
377
|
+
if (!tab) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const manualTitle = typeof tab.userDefinedLabel === 'string' ? tab.userDefinedLabel.trim() : '';
|
|
382
|
+
tab.title = manualTitle || getTabTitleFromQuery(tab.query, tabIndex);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function normalizeTab(tab, index) {
|
|
386
|
+
const normalizedTab = {
|
|
387
|
+
id: tab && tab.id ? tab.id : TabService.generateTabId(),
|
|
388
|
+
title: tab && typeof tab.title === 'string' ? tab.title : `${$ctrl.t('tabs.default')} ${index}`,
|
|
389
|
+
userDefinedLabel: tab && typeof tab.userDefinedLabel === 'string'
|
|
390
|
+
? tab.userDefinedLabel
|
|
391
|
+
: (tab && tab.hasManualTitle ? (tab.title || '') : ''),
|
|
392
|
+
query: tab && typeof tab.query === 'string' ? tab.query : $ctrl.t('query.placeholder'),
|
|
393
|
+
variables: tab && typeof tab.variables === 'string' ? tab.variables : '{}',
|
|
394
|
+
headers: tab && typeof tab.headers === 'string' ? tab.headers : '{}',
|
|
395
|
+
result: tab && typeof tab.result === 'string' ? tab.result : '',
|
|
396
|
+
isEditingTitle: false
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
applyAutomaticTabTitle(normalizedTab, index);
|
|
400
|
+
delete normalizedTab.hasManualTitle;
|
|
401
|
+
return normalizedTab;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function setTabs(nextTabs) {
|
|
405
|
+
const normalizedTabs = Array.isArray(nextTabs) && nextTabs.length
|
|
406
|
+
? nextTabs.map((tab, index) => normalizeTab(tab, index))
|
|
407
|
+
: [normalizeTab({}, 0)];
|
|
408
|
+
|
|
409
|
+
$ctrl.tabs = normalizedTabs;
|
|
410
|
+
TabService.saveTabs($ctrl.tabs);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function refreshActiveTabEditors() {
|
|
414
|
+
const activeTab = $ctrl.tabs[$ctrl.activeTab];
|
|
415
|
+
|
|
416
|
+
if (!activeTab) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
$timeout(() => {
|
|
421
|
+
if (activeTab.queryEditorApi && activeTab.queryEditorApi.refresh) {
|
|
422
|
+
activeTab.queryEditorApi.refresh();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (activeTab.variablesEditorApi && activeTab.variablesEditorApi.refresh) {
|
|
426
|
+
activeTab.variablesEditorApi.refresh();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (activeTab.headersEditorApi && activeTab.headersEditorApi.refresh) {
|
|
430
|
+
activeTab.headersEditorApi.refresh();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (activeTab.responseViewerApi && activeTab.responseViewerApi.refresh) {
|
|
434
|
+
activeTab.responseViewerApi.refresh();
|
|
435
|
+
}
|
|
436
|
+
}, 0);
|
|
437
|
+
|
|
438
|
+
$timeout(() => {
|
|
439
|
+
if (activeTab.queryEditorApi && activeTab.queryEditorApi.refresh) {
|
|
440
|
+
activeTab.queryEditorApi.refresh();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (activeTab.variablesEditorApi && activeTab.variablesEditorApi.refresh) {
|
|
444
|
+
activeTab.variablesEditorApi.refresh();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (activeTab.headersEditorApi && activeTab.headersEditorApi.refresh) {
|
|
448
|
+
activeTab.headersEditorApi.refresh();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (activeTab.responseViewerApi && activeTab.responseViewerApi.refresh) {
|
|
452
|
+
activeTab.responseViewerApi.refresh();
|
|
453
|
+
}
|
|
454
|
+
}, 120);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
$ctrl.beginTabTitleEdit = function (tabIndex, $event) {
|
|
458
|
+
const tab = $ctrl.tabs[tabIndex];
|
|
459
|
+
|
|
460
|
+
if (!tab) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if ($event) {
|
|
465
|
+
$event.preventDefault();
|
|
466
|
+
$event.stopPropagation();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
$ctrl.activeTab = tabIndex;
|
|
470
|
+
tab.isEditingTitle = true;
|
|
471
|
+
tab.titleDraft = (typeof tab.userDefinedLabel === 'string' && tab.userDefinedLabel.trim())
|
|
472
|
+
? tab.userDefinedLabel
|
|
473
|
+
: (tab.title || '');
|
|
474
|
+
|
|
475
|
+
$timeout(() => {
|
|
476
|
+
const input = document.getElementById(`tab-title-editor-${tab.id}`);
|
|
477
|
+
if (!input) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
input.focus();
|
|
482
|
+
input.select();
|
|
483
|
+
}, 0);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
$ctrl.commitTabTitleEdit = function (tabIndex) {
|
|
487
|
+
const tab = $ctrl.tabs[tabIndex];
|
|
488
|
+
|
|
489
|
+
if (!tab) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const nextTitle = (tab.titleDraft || '').trim();
|
|
494
|
+
tab.isEditingTitle = false;
|
|
495
|
+
|
|
496
|
+
tab.userDefinedLabel = nextTitle || '';
|
|
497
|
+
applyAutomaticTabTitle(tab, tabIndex);
|
|
498
|
+
|
|
499
|
+
delete tab.titleDraft;
|
|
500
|
+
$ctrl.persistTabs();
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
$ctrl.cancelTabTitleEdit = function (tabIndex) {
|
|
504
|
+
const tab = $ctrl.tabs[tabIndex];
|
|
505
|
+
|
|
506
|
+
if (!tab) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
tab.isEditingTitle = false;
|
|
511
|
+
delete tab.titleDraft;
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
$ctrl.handleTabTitleKeydown = function ($event, tabIndex) {
|
|
515
|
+
if ($event.key === 'Enter') {
|
|
516
|
+
$event.preventDefault();
|
|
517
|
+
$ctrl.commitTabTitleEdit(tabIndex);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if ($event.key === 'Escape') {
|
|
522
|
+
$event.preventDefault();
|
|
523
|
+
$ctrl.cancelTabTitleEdit(tabIndex);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
$ctrl.handleTabMouseDown = function ($event, tabIndex) {
|
|
528
|
+
if (!$event || $event.button !== 1 || $ctrl.tabs.length <= 1) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
$event.preventDefault();
|
|
533
|
+
$event.stopPropagation();
|
|
534
|
+
$ctrl.closeTab(tabIndex);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
$scope.$watch('$ctrl.tabs[$ctrl.activeTab].query', (newQuery) => {
|
|
538
|
+
const activeTab = $ctrl.tabs[$ctrl.activeTab];
|
|
539
|
+
|
|
540
|
+
if (!activeTab) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
applyAutomaticTabTitle(activeTab, $ctrl.activeTab);
|
|
545
|
+
|
|
546
|
+
$ctrl.persistTabs();
|
|
547
|
+
});
|
|
548
|
+
$scope.$watch('$ctrl.url', (newUrl) => {
|
|
549
|
+
debounce(() => {
|
|
550
|
+
loadSchema(newUrl);
|
|
551
|
+
sessionStorage.setItem('url', newUrl);
|
|
552
|
+
$scope.$apply();
|
|
553
|
+
}, 500)();
|
|
554
|
+
});
|
|
555
|
+
// Carrega as abas do sessionStorage
|
|
556
|
+
function loadTabs() {
|
|
557
|
+
const storedTabs = TabService.getTabs();
|
|
558
|
+
|
|
559
|
+
// Garante que cada aba tenha um ID único (evita duplicatas no ngRepeat)
|
|
560
|
+
setTabs(storedTabs);
|
|
561
|
+
|
|
562
|
+
const storedActiveTab = parseInt(sessionStorage.getItem(ACTIVE_TAB_STORAGE_KEY), 10);
|
|
563
|
+
if (Number.isInteger(storedActiveTab) && storedActiveTab >= 0) {
|
|
564
|
+
$ctrl.activeTab = Math.min(storedActiveTab, Math.max($ctrl.tabs.length - 1, 0));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function loadSchema(newUrl) {
|
|
568
|
+
console.log('Loading schema from', newUrl);
|
|
569
|
+
$ctrl.loadingSchema = true;
|
|
570
|
+
$ctrl.loadSchemaError = null;
|
|
571
|
+
const typeRefFragment = `
|
|
572
|
+
name
|
|
573
|
+
kind
|
|
574
|
+
ofType {
|
|
575
|
+
name
|
|
576
|
+
kind
|
|
577
|
+
ofType {
|
|
578
|
+
name
|
|
579
|
+
kind
|
|
580
|
+
ofType {
|
|
581
|
+
name
|
|
582
|
+
kind
|
|
583
|
+
ofType {
|
|
584
|
+
name
|
|
585
|
+
kind
|
|
586
|
+
ofType {
|
|
587
|
+
name
|
|
588
|
+
kind
|
|
589
|
+
ofType {
|
|
590
|
+
name
|
|
591
|
+
kind
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
`;
|
|
599
|
+
|
|
600
|
+
fetch(newUrl, {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: { 'Content-Type': 'application/json' },
|
|
603
|
+
body: JSON.stringify({
|
|
604
|
+
query: `
|
|
605
|
+
{
|
|
606
|
+
__schema {
|
|
607
|
+
description
|
|
608
|
+
queryType { name }
|
|
609
|
+
mutationType { name }
|
|
610
|
+
subscriptionType { name }
|
|
611
|
+
|
|
612
|
+
types {
|
|
613
|
+
name
|
|
614
|
+
description
|
|
615
|
+
kind
|
|
616
|
+
enumValues {
|
|
617
|
+
name
|
|
618
|
+
description
|
|
619
|
+
}
|
|
620
|
+
possibleTypes {
|
|
621
|
+
name
|
|
622
|
+
kind
|
|
623
|
+
}
|
|
624
|
+
inputFields {
|
|
625
|
+
name
|
|
626
|
+
description
|
|
627
|
+
defaultValue
|
|
628
|
+
type { ${typeRefFragment} }
|
|
629
|
+
}
|
|
630
|
+
fields {
|
|
631
|
+
name description
|
|
632
|
+
args {
|
|
633
|
+
name
|
|
634
|
+
description
|
|
635
|
+
defaultValue
|
|
636
|
+
type { ${typeRefFragment} }
|
|
637
|
+
}
|
|
638
|
+
type { ${typeRefFragment} }
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
` })
|
|
644
|
+
})
|
|
645
|
+
.then(res => res.json())
|
|
646
|
+
.then(result => {
|
|
647
|
+
$ctrl.schema = result.data.__schema;
|
|
648
|
+
$ctrl.loadSchemaError = null;
|
|
649
|
+
console.log('Schema loaded:', $ctrl.schema);
|
|
650
|
+
$timeout(() => {
|
|
651
|
+
$scope.$apply();
|
|
652
|
+
}, 0);
|
|
653
|
+
})
|
|
654
|
+
.catch(err => {
|
|
655
|
+
console.error('Error loading schema:', err);
|
|
656
|
+
$ctrl.schema = null;
|
|
657
|
+
$ctrl.loadSchemaError = 'Failed to load schema: ' + err.message;
|
|
658
|
+
$timeout(() => {
|
|
659
|
+
$scope.$apply();
|
|
660
|
+
}, 0);
|
|
661
|
+
})
|
|
662
|
+
.finally(() => {
|
|
663
|
+
$ctrl.loadingSchema = false;
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
$ctrl.loadSchema = function () {
|
|
667
|
+
loadSchema($ctrl.url);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
function parseOperationSnippet(queryText) {
|
|
671
|
+
const text = queryText || '';
|
|
672
|
+
const operationMatch = /^(query|mutation|subscription)\b\s*(\([^)]*\))?\s*\{/.exec(text.trim());
|
|
673
|
+
|
|
674
|
+
if (!operationMatch) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const operation = operationMatch[1];
|
|
679
|
+
const variableBlock = operationMatch[2] || '';
|
|
680
|
+
const bodyStart = text.indexOf('{');
|
|
681
|
+
const bodyEnd = text.lastIndexOf('}');
|
|
682
|
+
|
|
683
|
+
if (bodyStart === -1 || bodyEnd === -1 || bodyEnd <= bodyStart) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const body = text.slice(bodyStart + 1, bodyEnd).trim();
|
|
688
|
+
const fieldMatch = /^([A-Za-z_][A-Za-z0-9_]*)/.exec(body);
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
operation,
|
|
692
|
+
variableBlock,
|
|
693
|
+
body,
|
|
694
|
+
fieldName: fieldMatch ? fieldMatch[1] : null
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function mergeOperationIntoQuery(existingQuery, snippetQuery) {
|
|
699
|
+
const currentQuery = existingQuery || '';
|
|
700
|
+
const snippetInfo = parseOperationSnippet(snippetQuery);
|
|
701
|
+
|
|
702
|
+
if (!snippetInfo) {
|
|
703
|
+
return currentQuery ? `${currentQuery.replace(/\s+$/, '')}\n\n${snippetQuery}` : snippetQuery;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const operationRegex = new RegExp(`(^|\\n)\\s*${snippetInfo.operation}\\b([\\s\\S]*?)\\{`, 'm');
|
|
707
|
+
const operationMatch = operationRegex.exec(currentQuery);
|
|
708
|
+
|
|
709
|
+
if (!operationMatch) {
|
|
710
|
+
return currentQuery ? `${currentQuery.replace(/\s+$/, '')}\n\n${snippetQuery}` : snippetQuery;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const operationStart = operationMatch.index + operationMatch[1].length;
|
|
714
|
+
const headerStart = currentQuery.indexOf(snippetInfo.operation, operationStart);
|
|
715
|
+
const openBraceIndex = currentQuery.indexOf('{', headerStart);
|
|
716
|
+
|
|
717
|
+
if (openBraceIndex === -1) {
|
|
718
|
+
return currentQuery ? `${currentQuery.replace(/\s+$/, '')}\n\n${snippetQuery}` : snippetQuery;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
let depth = 0;
|
|
722
|
+
let closeBraceIndex = -1;
|
|
723
|
+
|
|
724
|
+
for (let index = openBraceIndex; index < currentQuery.length; index += 1) {
|
|
725
|
+
const char = currentQuery[index];
|
|
726
|
+
|
|
727
|
+
if (char === '{') {
|
|
728
|
+
depth += 1;
|
|
729
|
+
} else if (char === '}') {
|
|
730
|
+
depth -= 1;
|
|
731
|
+
|
|
732
|
+
if (depth === 0) {
|
|
733
|
+
closeBraceIndex = index;
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (closeBraceIndex === -1) {
|
|
740
|
+
return currentQuery ? `${currentQuery.replace(/\s+$/, '')}\n\n${snippetQuery}` : snippetQuery;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const operationBlock = currentQuery.slice(headerStart, closeBraceIndex + 1);
|
|
744
|
+
|
|
745
|
+
if (snippetInfo.fieldName && new RegExp(`\\b${snippetInfo.fieldName}\\b`).test(operationBlock)) {
|
|
746
|
+
return currentQuery;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
let updatedHeader = currentQuery.slice(headerStart, openBraceIndex);
|
|
750
|
+
const snippetVariables = (snippetInfo.variableBlock || '').trim();
|
|
751
|
+
|
|
752
|
+
if (snippetVariables) {
|
|
753
|
+
const existingVariableMatch = /\(([^)]*)\)\s*$/.exec(updatedHeader);
|
|
754
|
+
|
|
755
|
+
if (existingVariableMatch) {
|
|
756
|
+
const existingVariables = existingVariableMatch[1].trim();
|
|
757
|
+
const snippetVariablesContent = snippetVariables.slice(1, -1).trim();
|
|
758
|
+
const existingDefinitions = existingVariables ? existingVariables.split(/\s*,\s*/) : [];
|
|
759
|
+
const nextDefinitions = [...existingDefinitions];
|
|
760
|
+
|
|
761
|
+
snippetVariablesContent.split(/\s*,\s*/).filter(Boolean).forEach((definition) => {
|
|
762
|
+
if (!nextDefinitions.includes(definition)) {
|
|
763
|
+
nextDefinitions.push(definition);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
updatedHeader = updatedHeader.replace(/\(([^)]*)\)\s*$/, `(${nextDefinitions.join(', ')}) `);
|
|
768
|
+
} else {
|
|
769
|
+
updatedHeader = `${updatedHeader.trimEnd()} ${snippetVariables} `;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const operationBody = currentQuery.slice(openBraceIndex + 1, closeBraceIndex).replace(/\s+$/, '');
|
|
774
|
+
const nextBody = operationBody
|
|
775
|
+
? `${operationBody}\n ${snippetInfo.body}`
|
|
776
|
+
: `\n ${snippetInfo.body}`;
|
|
777
|
+
|
|
778
|
+
return `${currentQuery.slice(0, headerStart)}${updatedHeader}{${nextBody}\n}${currentQuery.slice(closeBraceIndex + 1)}`;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function mergeVariablesJson(existingVariablesText, snippetVariablesText) {
|
|
782
|
+
try {
|
|
783
|
+
const existing = JSON.parse(existingVariablesText || '{}');
|
|
784
|
+
const incoming = JSON.parse(snippetVariablesText || '{}');
|
|
785
|
+
|
|
786
|
+
if (!existing || Array.isArray(existing) || typeof existing !== 'object') {
|
|
787
|
+
return snippetVariablesText;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (!incoming || Array.isArray(incoming) || typeof incoming !== 'object') {
|
|
791
|
+
return existingVariablesText;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return JSON.stringify({ ...incoming, ...existing }, null, 2);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
return existingVariablesText || snippetVariablesText;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
$ctrl.insertSchemaOperation = function (snippet) {
|
|
801
|
+
const activeTab = $ctrl.tabs[$ctrl.activeTab];
|
|
802
|
+
|
|
803
|
+
if (!activeTab || !snippet || !snippet.query) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const currentQuery = (activeTab.query || '').trim();
|
|
808
|
+
const isPlaceholderQuery = !currentQuery || currentQuery === $ctrl.t('query.placeholder');
|
|
809
|
+
|
|
810
|
+
const nextQuery = isPlaceholderQuery
|
|
811
|
+
? snippet.query
|
|
812
|
+
: mergeOperationIntoQuery(activeTab.query, snippet.query);
|
|
813
|
+
const nextVariables = snippet.variables
|
|
814
|
+
? mergeVariablesJson(activeTab.variables, snippet.variables)
|
|
815
|
+
: activeTab.variables;
|
|
816
|
+
|
|
817
|
+
$ctrl.tabs[$ctrl.activeTab] = {
|
|
818
|
+
...activeTab,
|
|
819
|
+
query: nextQuery,
|
|
820
|
+
variables: nextVariables
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
$ctrl.persistTabs();
|
|
824
|
+
|
|
825
|
+
$timeout(() => {
|
|
826
|
+
$scope.$applyAsync();
|
|
827
|
+
}, 0);
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
$ctrl.send = async function () {
|
|
831
|
+
const activeTab = $ctrl.tabs[$ctrl.activeTab];
|
|
832
|
+
|
|
833
|
+
if (!activeTab) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const rawQuery = typeof activeTab.query === 'string' ? activeTab.query.trim() : '';
|
|
838
|
+
if (!rawQuery || rawQuery === $ctrl.t('query.placeholder')) {
|
|
839
|
+
activeTab.result = JSON.stringify({ error: 'Query is empty.' }, null, 2);
|
|
840
|
+
$ctrl.persistTabs();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
activeTab.result = 'Loading...';
|
|
845
|
+
$ctrl.persistTabs();
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
const variables = parseJsonObject(activeTab.variables, {});
|
|
849
|
+
const requestHeaders = parseJsonObject(activeTab.headers, {});
|
|
850
|
+
const mergedHeaders = {
|
|
851
|
+
'Content-Type': 'application/json',
|
|
852
|
+
...getSharedHeadersMap(),
|
|
853
|
+
...requestHeaders
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const response = await fetch($ctrl.url, {
|
|
857
|
+
method: 'POST',
|
|
858
|
+
headers: mergedHeaders,
|
|
859
|
+
body: JSON.stringify({
|
|
860
|
+
query: activeTab.query,
|
|
861
|
+
variables
|
|
862
|
+
})
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
const formattedBody = await formatResponseBody(response, {
|
|
866
|
+
status: response.status,
|
|
867
|
+
statusText: response.statusText
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
activeTab.result = formattedBody;
|
|
871
|
+
} catch (error) {
|
|
872
|
+
activeTab.result = JSON.stringify({
|
|
873
|
+
error: error && error.message ? error.message : 'Request failed.'
|
|
874
|
+
}, null, 2);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
$ctrl.persistTabs();
|
|
878
|
+
$timeout(() => {
|
|
879
|
+
$scope.$applyAsync();
|
|
880
|
+
}, 0);
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
$ctrl.downloadWorkspace = function () {
|
|
884
|
+
const exportPayload = {
|
|
885
|
+
version: 1,
|
|
886
|
+
exportedAt: new Date().toISOString(),
|
|
887
|
+
locale: I18nService.getLocale(),
|
|
888
|
+
url: $ctrl.url,
|
|
889
|
+
activeTab: $ctrl.activeTab,
|
|
890
|
+
sharedHeaders: sanitizeSharedHeadersForExport(AppState.getKey(SHARED_HEADERS_STORAGE_KEY) || []),
|
|
891
|
+
tabs: ($ctrl.tabs || []).map((tab) => ({
|
|
892
|
+
id: tab.id,
|
|
893
|
+
title: tab.title,
|
|
894
|
+
userDefinedLabel: tab.userDefinedLabel || '',
|
|
895
|
+
query: tab.query || '',
|
|
896
|
+
variables: tab.variables || '{}',
|
|
897
|
+
headers: sanitizeHeadersTextForExport(tab.headers),
|
|
898
|
+
result: tab.result || ''
|
|
899
|
+
}))
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const json = JSON.stringify(exportPayload, null, 2);
|
|
903
|
+
const blob = new Blob([json], { type: 'application/json;charset=utf-8' });
|
|
904
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
905
|
+
const link = document.createElement('a');
|
|
906
|
+
|
|
907
|
+
link.href = objectUrl;
|
|
908
|
+
link.download = 'graphql-playground-workspace.json';
|
|
909
|
+
document.body.appendChild(link);
|
|
910
|
+
link.click();
|
|
911
|
+
document.body.removeChild(link);
|
|
912
|
+
URL.revokeObjectURL(objectUrl);
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
$ctrl.importWorkspace = function (workspace) {
|
|
916
|
+
if (!workspace || typeof workspace !== 'object') {
|
|
917
|
+
throw new Error(getWorkspaceLabel('importError'));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const nextLocale = typeof workspace.locale === 'string' ? workspace.locale : I18nService.getLocale();
|
|
921
|
+
$ctrl.locale = I18nService.setLocale(nextLocale);
|
|
922
|
+
|
|
923
|
+
if (Array.isArray(workspace.sharedHeaders)) {
|
|
924
|
+
AppState.saveKey(SHARED_HEADERS_STORAGE_KEY, workspace.sharedHeaders.map((header) => ({
|
|
925
|
+
key: header && typeof header.key === 'string' ? header.key : '',
|
|
926
|
+
value: header && typeof header.value === 'string' ? header.value : ''
|
|
927
|
+
})));
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
setTabs(Array.isArray(workspace.tabs) ? workspace.tabs : []);
|
|
931
|
+
$ctrl.activeTab = Number.isInteger(workspace.activeTab)
|
|
932
|
+
? Math.min(Math.max(workspace.activeTab, 0), Math.max($ctrl.tabs.length - 1, 0))
|
|
933
|
+
: 0;
|
|
934
|
+
$ctrl.url = typeof workspace.url === 'string' && workspace.url.trim()
|
|
935
|
+
? workspace.url
|
|
936
|
+
: $ctrl.url;
|
|
937
|
+
|
|
938
|
+
sessionStorage.setItem(ACTIVE_TAB_STORAGE_KEY, String($ctrl.activeTab || 0));
|
|
939
|
+
sessionStorage.setItem('url', $ctrl.url);
|
|
940
|
+
TabService.saveTabs($ctrl.tabs);
|
|
941
|
+
$ctrl.showConfig = false;
|
|
942
|
+
loadSchema($ctrl.url);
|
|
943
|
+
$ctrl.persistTabs();
|
|
944
|
+
|
|
945
|
+
$timeout(() => {
|
|
946
|
+
$scope.$applyAsync();
|
|
947
|
+
}, 0);
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// Adiciona uma nova aba
|
|
951
|
+
$ctrl.addTab = function () {
|
|
952
|
+
const newTab = {
|
|
953
|
+
id: TabService.generateTabId(),
|
|
954
|
+
title: `${$ctrl.t('tabs.default')} ${$ctrl.tabs.length}`,
|
|
955
|
+
userDefinedLabel: '',
|
|
956
|
+
query: $ctrl.t('query.placeholder'),
|
|
957
|
+
variables: '{}',
|
|
958
|
+
headers: '{}',
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
$ctrl.tabs.push(newTab);
|
|
962
|
+
TabService.saveTabs($ctrl.tabs);
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// Fecha uma aba
|
|
966
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
967
|
+
$ctrl.closeTab = function (index) {
|
|
968
|
+
if ($ctrl.tabs.length <= 1) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
$ctrl.tabs.splice(index, 1);
|
|
973
|
+
$ctrl.activeTab = clamp($ctrl.activeTab, 0, $ctrl.tabs.length - 1);
|
|
974
|
+
sessionStorage.setItem(ACTIVE_TAB_STORAGE_KEY, String($ctrl.activeTab || 0));
|
|
975
|
+
TabService.saveTabs($ctrl.tabs);
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// Inicializa abas carregadas do sessionStorage
|
|
979
|
+
loadTabs();
|
|
980
|
+
if ($ctrl.tabs.length === 0) {
|
|
981
|
+
$ctrl.addTab();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
$scope.$watch('$ctrl.activeTab', (newActiveTab) => {
|
|
985
|
+
if (!Number.isInteger(newActiveTab) || newActiveTab < 0) {
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
sessionStorage.setItem(ACTIVE_TAB_STORAGE_KEY, String(newActiveTab));
|
|
990
|
+
refreshActiveTabEditors();
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// Funções auxiliares para atalhos de teclado
|
|
994
|
+
const handleKeyHelpers = {
|
|
995
|
+
isCtrlSpace: (event) => event.which === 32 && event.ctrlKey,
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// Manipuladores de eventos de teclado
|
|
999
|
+
function handleKeyEvent(event, type) {
|
|
1000
|
+
if (handleKeyHelpers.isCtrlSpace(event)) {
|
|
1001
|
+
console.log('ctrl+space (autocomplete for)', type);
|
|
1002
|
+
// TODO: Implement autocomplete
|
|
1003
|
+
} else {
|
|
1004
|
+
// console.log(`${type} keydown:`, event.key, event.keyCode, event.keyChar, event);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
$ctrl.handleQueryKeydown = (event) => handleKeyEvent(event, 'Query');
|
|
1009
|
+
$ctrl.handleVariablesKeydown = (event) => handleKeyEvent(event, 'Variables');
|
|
1010
|
+
$ctrl.handleHeadersKeydown = (event) => handleKeyEvent(event, 'Headers');
|
|
1011
|
+
|
|
1012
|
+
// Garante a sincronização do estado com o AngularJS caso necessário
|
|
1013
|
+
$timeout(() => {
|
|
1014
|
+
$scope.$apply();
|
|
1015
|
+
}, 0);
|
|
1016
|
+
}]);
|