This commit is contained in:
Julian Freeman
2026-04-19 08:55:44 -04:00
parent eb1d802f5b
commit e256e596c5
7 changed files with 161 additions and 80 deletions

View File

@@ -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) => {
<td class="py-1 pl-2 align-middle">
<input
type="checkbox"
v-model="item.enabled"
:checked="item.enabled"
@change="updateRow(index, 'enabled', ($event.target as HTMLInputElement).checked)"
class="rounded bg-slate-800 border-slate-700 text-indigo-500 focus:ring-indigo-500/50 focus:ring-offset-0"
>
</td>
<td class="py-1 pr-2 align-middle">
<input
type="text"
v-model="item.key"
:value="item.key"
@input="updateRow(index, 'key', ($event.target as HTMLInputElement).value)"
placeholder="Key"
class="w-full bg-transparent border-b border-transparent hover:border-slate-700 focus:border-indigo-500 focus:outline-none py-1 px-1 text-sm text-slate-300 transition-colors"
>
@@ -62,7 +72,8 @@ const removeRow = (index: number) => {
<td class="py-1 pr-2 align-middle">
<input
type="text"
v-model="item.value"
:value="item.value"
@input="updateRow(index, 'value', ($event.target as HTMLInputElement).value)"
placeholder="Value"
class="w-full bg-transparent border-b border-transparent hover:border-slate-700 focus:border-indigo-500 focus:outline-none py-1 px-1 text-sm text-slate-300 transition-colors"
>

View File

@@ -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;

View File

@@ -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;
}
});

View File

@@ -68,7 +68,6 @@ const displayFontOptions = computed(() => {
placeholder="Select or enter font family"
/>
<input
v-if="!fontOptions.includes(settings.editorFontFamily)"
type="text"
v-model="settings.editorFontFamily"
placeholder="e.g. 'JetBrains Mono', monospace"

View File

@@ -33,34 +33,110 @@ export interface RequestData {
timestamp: number;
}
const createEmptyKeyValue = (): KeyValue => ({
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<AuthState> | 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>): 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>): 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<RequestData[]>([]);
const activeRequest = ref<RequestData>({
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<RequestData>(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;
};