diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index b21bd68..5467e21 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -1,6 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ +/target*/ # Generated by Tauri # will have schema files for capabilities auto-completion diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 92edba2..668674b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::time::Instant; use serde::{Serialize, Deserialize}; +use tauri::State; #[derive(Serialize, Deserialize)] struct HttpResponse { @@ -39,8 +40,13 @@ struct AuthConfig { api_key: ApiKeyAuth, } +struct AppState { + client: reqwest::Client, +} + #[tauri::command] async fn execute_request( + state: State<'_, AppState>, method: String, url: String, headers: HashMap, @@ -48,14 +54,8 @@ async fn execute_request( query_params: Option>, auth: Option, ) -> Result { - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .map_err(|e| e.to_string())?; - let req_method = method.parse::().map_err(|e| e.to_string())?; - - let mut request_builder = client.request(req_method, &url); + let mut request_builder = state.client.request(req_method, &url); // Add Query Params if let Some(params) = query_params { @@ -126,9 +126,15 @@ async fn execute_request( #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .expect("failed to create HTTP client"); + tauri::Builder::default() + .manage(AppState { client }) .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![execute_request]) .run(tauri::generate_context!()) .expect("error while running tauri application"); -} \ No newline at end of file +} diff --git a/src/components/KeyValueEditor.vue b/src/components/KeyValueEditor.vue index 0b606fe..c263c59 100644 --- a/src/components/KeyValueEditor.vue +++ b/src/components/KeyValueEditor.vue @@ -9,6 +9,14 @@ const props = defineProps<{ const emit = defineEmits(['update:modelValue']); +const updateRow = (index: number, key: keyof KeyValue, value: string | boolean) => { + const newData = props.modelValue.map((item, itemIndex) => { + if (itemIndex !== index) return item; + return { ...item, [key]: value }; + }); + emit('update:modelValue', newData); +}; + const addRow = () => { const newData = [...props.modelValue, { id: crypto.randomUUID(), @@ -47,14 +55,16 @@ const removeRow = (index: number) => { @@ -62,7 +72,8 @@ const removeRow = (index: number) => { diff --git a/src/components/RequestPanel.vue b/src/components/RequestPanel.vue index 9d9c782..c8912ef 100644 --- a/src/components/RequestPanel.vue +++ b/src/components/RequestPanel.vue @@ -37,7 +37,7 @@ const extensions = computed(() => { const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; const executeRequest = async () => { - if (!store.activeRequest.url) return; + if (isLoading.value || !store.activeRequest.url) return; isLoading.value = true; @@ -73,7 +73,6 @@ const executeRequest = async () => { } catch (error) { console.error(error); - // Simple alert for now, or could use a toast alert('Request Failed: ' + error); } finally { isLoading.value = false; diff --git a/src/components/ResponsePanel.vue b/src/components/ResponsePanel.vue index 11dbee2..da336e2 100644 --- a/src/components/ResponsePanel.vue +++ b/src/components/ResponsePanel.vue @@ -12,6 +12,23 @@ const store = useRequestStore(); const settings = useSettingsStore(); const isCopied = ref(false); +const isJsonResponse = computed(() => { + const response = store.activeRequest.response; + if (!response) return false; + + const contentType = response.headers['content-type'] || response.headers['Content-Type'] || ''; + if (contentType.toLowerCase().includes('json')) { + return true; + } + + try { + JSON.parse(response.body); + return true; + } catch { + return false; + } +}); + const extensions = computed(() => { const theme = EditorView.theme({ "&": { @@ -26,15 +43,20 @@ const extensions = computed(() => { } }); - return [json(), oneDark, theme, EditorView.editable.of(false)]; + return [ + oneDark, + theme, + EditorView.editable.of(false), + ...(isJsonResponse.value ? [json()] : []), + ]; }); const formattedBody = computed(() => { if (!store.activeRequest.response) return ''; + if (!isJsonResponse.value) return store.activeRequest.response.body; try { - // Auto pretty print return JSON.stringify(JSON.parse(store.activeRequest.response.body), null, 2); - } catch (e) { + } catch { return store.activeRequest.response.body; } }); diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index 1f9af24..a43bd1c 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -68,7 +68,6 @@ const displayFontOptions = computed(() => { placeholder="Select or enter font family" /> ({ + id: crypto.randomUUID(), + key: '', + value: '', + enabled: true, +}); + +const createDefaultAuthState = (): AuthState => ({ + type: 'none', + basic: { username: '', password: '' }, + bearer: { token: '' }, + apiKey: { key: '', value: '', addTo: 'header' }, +}); + +const createDefaultRequest = (): RequestData => ({ + id: crypto.randomUUID(), + method: 'GET', + url: '', + params: [createEmptyKeyValue()], + headers: [createEmptyKeyValue()], + body: '', + auth: createDefaultAuthState(), + timestamp: Date.now(), +}); + +const cloneKeyValueList = (items: KeyValue[] | undefined): KeyValue[] => { + if (!Array.isArray(items) || items.length === 0) return []; + return items.map((item) => ({ + id: item.id || crypto.randomUUID(), + key: item.key || '', + value: item.value || '', + enabled: item.enabled ?? true, + })); +}; + +const cloneAuthState = (auth: Partial | undefined): AuthState => ({ + type: auth?.type || 'none', + basic: { + username: auth?.basic?.username || '', + password: auth?.basic?.password || '', + }, + bearer: { + token: auth?.bearer?.token || '', + }, + apiKey: { + key: auth?.apiKey?.key || '', + value: auth?.apiKey?.value || '', + addTo: auth?.apiKey?.addTo || 'header', + }, +}); + +const cloneRequest = (req: Partial): RequestData => ({ + id: typeof req.id === 'string' && req.id ? req.id : crypto.randomUUID(), + method: req.method || 'GET', + url: req.url || '', + params: cloneKeyValueList(req.params), + headers: cloneKeyValueList(req.headers), + body: req.body || '', + auth: cloneAuthState(req.auth), + response: req.response + ? { + status: req.response.status, + headers: { ...req.response.headers }, + body: req.response.body, + time: req.response.time, + size: req.response.size, + } + : undefined, + timestamp: typeof req.timestamp === 'number' ? req.timestamp : Date.now(), +}); + +const normalizeHistoryItem = (item: Partial): RequestData => { + const cloned = cloneRequest(item); + + return { + id: cloned.id, + method: cloned.method, + url: cloned.url, + params: cloned.params.length > 0 + ? cloned.params + : [createEmptyKeyValue()], + headers: cloned.headers.length > 0 + ? cloned.headers + : [createEmptyKeyValue()], + body: cloned.body, + auth: cloned.auth, + response: cloned.response, + timestamp: cloned.timestamp, + }; +}; + export const useRequestStore = defineStore('request', () => { // State const history = ref([]); - const activeRequest = ref({ - id: crypto.randomUUID(), - method: 'GET', - url: '', - params: [ - { id: crypto.randomUUID(), key: '', value: '', enabled: true } - ], - headers: [ - { id: crypto.randomUUID(), key: '', value: '', enabled: true } - ], - body: '', - auth: { - type: 'none', - basic: { username: '', password: '' }, - bearer: { token: '' }, - apiKey: { key: '', value: '', addTo: 'header' } - }, - timestamp: Date.now(), - }); + const activeRequest = ref(createDefaultRequest()); // Load history from local storage const savedHistory = localStorage.getItem('request_history'); if (savedHistory) { try { - history.value = JSON.parse(savedHistory); + const parsed = JSON.parse(savedHistory); + history.value = Array.isArray(parsed) + ? parsed.map((item) => normalizeHistoryItem(item)) + : []; } catch (e) { console.error('Failed to parse history', e); } @@ -68,13 +144,12 @@ export const useRequestStore = defineStore('request', () => { // Actions const addToHistory = (req: RequestData) => { - // Add to beginning, limit to 50 - const newEntry = { ...req, timestamp: Date.now() }; - // Don't store the empty trailing rows in history to save space? - // Actually keep them for consistency or filter them. Let's filter empty keys. - newEntry.params = newEntry.params.filter(p => p.key.trim() !== ''); - newEntry.headers = newEntry.headers.filter(h => h.key.trim() !== ''); - + const newEntry = cloneRequest(req); + newEntry.id = crypto.randomUUID(); + newEntry.timestamp = Date.now(); + newEntry.params = newEntry.params.filter((p) => p.key.trim() !== ''); + newEntry.headers = newEntry.headers.filter((h) => h.key.trim() !== ''); + history.value.unshift(newEntry); if (history.value.length > 50) { history.value.pop(); @@ -93,46 +168,14 @@ export const useRequestStore = defineStore('request', () => { }; const resetActiveRequest = () => { - activeRequest.value = { - id: crypto.randomUUID(), - method: 'GET', - url: '', - params: [ - { id: crypto.randomUUID(), key: '', value: '', enabled: true } - ], - headers: [ - { id: crypto.randomUUID(), key: '', value: '', enabled: true } - ], - body: '', - auth: { - type: 'none', - basic: { username: '', password: '' }, - bearer: { token: '' }, - apiKey: { key: '', value: '', addTo: 'header' } - }, - timestamp: Date.now(), - }; + activeRequest.value = createDefaultRequest(); }; const loadRequest = (req: RequestData) => { - // Deep copy - const loaded = JSON.parse(JSON.stringify(req)); + const loaded = normalizeHistoryItem(cloneRequest(req)); loaded.id = crypto.randomUUID(); - // loaded.response = undefined; // Keep response for history viewing - - // Ensure at least one empty row for editing - if (loaded.params.length === 0) loaded.params.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true }); - if (loaded.headers.length === 0) loaded.headers.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true }); - - // Ensure auth object exists (migration for old history) - if (!loaded.auth) { - loaded.auth = { - type: 'none', - basic: { username: '', password: '' }, - bearer: { token: '' }, - apiKey: { key: '', value: '', addTo: 'header' } - }; - } + if (loaded.params.length === 0) loaded.params.push(createEmptyKeyValue()); + if (loaded.headers.length === 0) loaded.headers.push(createEmptyKeyValue()); activeRequest.value = loaded; };