Kaynağa Gözat

add header enabled functionality and workspace entry sections

biblius 1 hafta önce
ebeveyn
işleme
ee2882bb07

+ 14 - 0
src-tauri/src/cmd.rs

@@ -267,6 +267,8 @@ pub async fn update_query_param_enabled(
                 .await
                 .map_err(|e| e.to_string())?;
 
+            // This acts as a toggle, if the position is present, remove the query param
+            // located at it
             if let Some(position) = qp.position {
                 let Some((removed, offset)) = url.remove_query_param(position as usize) else {
                     return Err(format!("no query param at position {position}"));
@@ -700,6 +702,18 @@ pub async fn update_header(
     Ok(())
 }
 
+#[tauri::command]
+pub async fn update_header_enabled(
+    state: tauri::State<'_, AppState>,
+    header_id: i64,
+    enabled: bool,
+) -> Result<(), String> {
+    if let Err(e) = db::update_header_enabled(state.db.clone(), header_id, enabled).await {
+        return Err(e.to_string());
+    }
+    Ok(())
+}
+
 #[tauri::command]
 pub async fn delete_header(
     state: tauri::State<'_, AppState>,

+ 14 - 11
src-tauri/src/db.rs

@@ -38,15 +38,6 @@ impl<T: Clone> Clone for Update<T> {
 
 impl<T: Copy> Copy for Update<T> {}
 
-impl<T: Copy> Update<T> {
-    pub fn value(self) -> Option<T> {
-        match self {
-            Update::Value(v) => Some(v),
-            Update::Null => None,
-        }
-    }
-}
-
 pub async fn init(url: &str) -> SqlitePool {
     let mut opts = SqliteConnectOptions::from_str(url).unwrap();
 
@@ -517,7 +508,7 @@ pub async fn get_workspace_request(db: SqlitePool, id: i64) -> AppResult<Workspa
 
     let headers = sqlx::query_as!(
         RequestHeader,
-        "SELECT id, name, value FROM request_headers WHERE request_id = ?",
+        "SELECT id, name, value, enabled FROM request_headers WHERE request_id = ?",
         entry.id
     )
     .fetch_all(&db)
@@ -800,12 +791,24 @@ pub async fn insert_headers(
             .push_bind(header.value);
     });
     Ok(insert
-        .push("RETURNING id, name, value")
+        .push("RETURNING id, name, value, enabled")
         .build_query_as()
         .fetch_one(&db)
         .await?)
 }
 
+pub async fn update_header_enabled(db: SqlitePool, id: i64, enabled: bool) -> AppResult<()> {
+    sqlx::query!(
+        "UPDATE request_headers SET enabled = ? WHERE id = ?",
+        enabled,
+        id
+    )
+    .execute(&db)
+    .await?;
+
+    Ok(())
+}
+
 pub async fn update_header(db: SqlitePool, header: RequestHeaderUpdate) -> AppResult<()> {
     sqlx::query!(
         "UPDATE request_headers SET name = COALESCE(?, ''), value = COALESCE(?, '') WHERE id = ?",

+ 1 - 0
src-tauri/src/lib.rs

@@ -68,6 +68,7 @@ pub fn run() {
             cmd::delete_env_var,
             cmd::insert_header,
             cmd::update_header,
+            cmd::update_header_enabled,
             cmd::delete_header,
             cmd::insert_request_body,
             cmd::update_request_body,

+ 12 - 29
src-tauri/src/request.rs

@@ -8,7 +8,7 @@ use crate::{
     error::AppError,
     request::{
         ctype::ContentType,
-        url::{QueryParam, RequestUrl, Segment},
+        url::{QueryParam, RequestUrl},
     },
     workspace::WorkspaceEntryBase,
     AppResult,
@@ -44,6 +44,7 @@ pub async fn send(client: reqwest::Client, req: HttpRequestParameters) -> AppRes
 
     let body = match body {
         Some(body) => {
+            // Parse each body with respective parser to ensure valid syntax
             match body.ty {
                 ContentType::Text => insert_ct_if_missing(&mut headers, "text/plain"),
                 ContentType::Json => {
@@ -95,30 +96,6 @@ pub async fn send(client: reqwest::Client, req: HttpRequestParameters) -> AppRes
     Ok(res)
 }
 
-/// Load the request body into a vec, validating it beforehand to ensure no syntactic errors are
-/// present in bodies that need valid syntax.
-pub async fn get_valid_request_body(path: &str, ty: ContentType) -> AppResult<String> {
-    let body = tokio::fs::read_to_string(path).await?;
-
-    match ty {
-        ContentType::Text => {}
-        ContentType::Json => {
-            serde_json::from_str::<serde_json::Value>(&body)?;
-        }
-        ContentType::Xml => {
-            roxmltree::Document::parse(&body)?;
-        }
-        ContentType::FormUrlEncoded => {
-            serde_urlencoded::from_str::<Vec<(&str, &str)>>(&body)
-                .map_err(|e| AppError::SerdeUrl(e.to_string()))?;
-        }
-        // Handled by reqwest
-        ContentType::FormData => {}
-    };
-
-    Ok(body)
-}
-
 #[derive(Debug, Serialize)]
 pub struct WorkspaceRequest {
     /// Workspace entry representing this request.
@@ -186,6 +163,7 @@ impl WorkspaceRequest {
                         id: -1,
                         name: token.name,
                         value: token.value,
+                        enabled: true,
                     });
                 }
             },
@@ -198,6 +176,7 @@ impl WorkspaceRequest {
                     id: -1,
                     name: "Authorization".to_string(),
                     value,
+                    enabled: true,
                 });
             }
             crate::auth::Auth::OAuth(OAuth {
@@ -241,10 +220,13 @@ impl TryFrom<WorkspaceRequest> for HttpRequestParameters {
         let mut headers = HeaderMap::new();
 
         for header in value.headers {
-            headers.insert(
-                reqwest::header::HeaderName::from_str(&header.name).map_err(|e| e.to_string())?,
-                HeaderValue::from_str(&header.value).map_err(|e| e.to_string())?,
-            );
+            if header.enabled {
+                headers.insert(
+                    reqwest::header::HeaderName::from_str(&header.name)
+                        .map_err(|e| e.to_string())?,
+                    HeaderValue::from_str(&header.value).map_err(|e| e.to_string())?,
+                );
+            }
         }
 
         Ok(Self {
@@ -450,6 +432,7 @@ pub struct RequestHeader {
     pub id: i64,
     pub name: String,
     pub value: String,
+    pub enabled: bool,
 }
 
 #[derive(Debug, Deserialize)]

+ 41 - 0
src/lib/components/KeyValInput.svelte

@@ -0,0 +1,41 @@
+<script lang="ts">
+  import { Input } from "$lib/components/ui/input";
+
+  type Props = {
+    class?: string;
+    onInput: (key: string, value: string) => Promise<void>;
+  };
+
+  let key: string = $state("");
+  let value: string = $state("");
+  let inputTimeout: number | undefined = $state();
+
+  let { class: className = "", onInput }: Props = $props();
+
+  function handleInput() {
+    if (inputTimeout != undefined) {
+      clearTimeout(inputTimeout);
+    }
+
+    inputTimeout = setTimeout(async () => {
+      await onInput(key, value);
+      key = "";
+      value = "";
+    }, 200);
+  }
+</script>
+
+<div class={"flex gap-2 " + className}>
+  <Input
+    class="col-start-2"
+    bind:value={key}
+    placeholder="Key"
+    oninput={handleInput}
+  />
+  <Input
+    class="col-start-3"
+    bind:value
+    placeholder="Value"
+    oninput={handleInput}
+  />
+</div>

+ 129 - 138
src/lib/components/WorkspaceEntry.svelte

@@ -1,5 +1,4 @@
 <script lang="ts">
-  let { requestPane = $bindable(), responsePane = $bindable() } = $props();
   import { Clipboard } from "@lucide/svelte";
   import * as Select from "$lib/components/ui/select";
   import {
@@ -15,6 +14,7 @@
     updateBodyContent,
     updateEntryName,
     updateHeader,
+    updateHeaderEnabled,
     updateQueryParamEnabled,
     updateRequestMethod,
     updateUrl,
@@ -37,6 +37,10 @@
   import Response from "./Response.svelte";
   import Checkbox from "./ui/checkbox/checkbox.svelte";
   import { tick } from "svelte";
+  import KeyValInput from "./KeyValInput.svelte";
+  import WorkspaceEntrySection from "./WorkspaceEntrySection.svelte";
+
+  let { requestPane = $bindable(), responsePane = $bindable() } = $props();
 
   let isSending = $derived.by(isRequestSending);
 
@@ -123,39 +127,40 @@
     }, 200);
   }
 
-  function handleAddQueryParam() {
-    if (addQueryTimeout != undefined) {
-      clearTimeout(addQueryTimeout);
+  async function handleAddHeader(key: string, val: string) {
+    const headerId = await insertHeader(key, val);
+
+    await tick(); // wait for DOM update
+
+    if (key) {
+      document.getElementById(`${headerId}_header_key`)?.focus();
+    } else {
+      document.getElementById(`${headerId}_header_val`)?.focus();
     }
-    addQueryTimeout = setTimeout(async () => {
-      const key = addQueryParamKeyInput ? addQueryParamKeyInput : "";
-      const val = addQueryParamValInput ? addQueryParamValInput : "";
-
-      if (_state.entry.query.length === 0) {
-        _state.entry.url += `?${key}=${val}`;
-      } else {
-        _state.entry.url += `&${key}=${val}`;
-      }
+  }
 
-      await updateUrl({
-        type: "URL",
-        url: _state.entry.url,
-      });
+  async function handleAddQueryParam(key: string, val: string) {
+    if (_state.entry.query.length === 0) {
+      _state.entry.url += `?${key}=${val}`;
+    } else {
+      _state.entry.url += `&${key}=${val}`;
+    }
 
-      await tick(); // wait for DOM update
+    await updateUrl({
+      type: "URL",
+      url: _state.entry.url,
+    });
 
-      addQueryParamValInput = "";
-      addQueryParamKeyInput = "";
+    await tick(); // wait for DOM update
 
-      // Added param will always be last
-      const param = _state.entry.query[_state.entry.query.length - 1];
+    // Added param will always be last
+    const param = _state.entry.query[_state.entry.query.length - 1];
 
-      if (key) {
-        document.getElementById(`${param.id}_query_key`)?.focus();
-      } else {
-        document.getElementById(`${param.id}_query_val`)?.focus();
-      }
-    }, 200);
+    if (key) {
+      document.getElementById(`${param.id}_query_key`)?.focus();
+    } else {
+      document.getElementById(`${param.id}_query_val`)?.focus();
+    }
   }
 </script>
 
@@ -337,64 +342,61 @@
         <Tabs.Root value="params" class="h-full flex flex-col">
           <Tabs.List class="shrink-0">
             <Tabs.Trigger value="params">Parameters</Tabs.Trigger>
-            <Tabs.Trigger value="headers">Headers</Tabs.Trigger>
             <Tabs.Trigger value="auth">Auth</Tabs.Trigger>
           </Tabs.List>
 
           <div class="flex-1 overflow-auto p-2">
             <!-- ================= PARAMETERS ================= -->
 
-            <!-- ================= HEADERS ================= -->
-
-            <div>
-              <h3
-                class="mb-2 pb-1 pointer-events-none text-xs font-medium border-b border-secondary text-muted-foreground"
-              >
-                Headers
-              </h3>
-              <div
-                class="grid grid-cols-[2%_1fr_1fr_2%] items-center justify-center gap-2 text-sm"
-              >
-                {#each _state.entry.headers as header (header.id)}
-                  <Input
-                    class="col-start-2"
-                    bind:value={header.name}
-                    placeholder="Name"
-                    oninput={() =>
-                      updateHeader(header.id, header.name, header.value)}
-                  />
+            <Tabs.Content value="params" class="space-y-4">
+              <!-- ================= HEADERS ================= -->
 
-                  <Input
-                    class="col-start-3"
-                    bind:value={header.value}
-                    placeholder="Value"
-                    oninput={() =>
-                      updateHeader(header.id, header.name, header.value)}
-                  />
+              <WorkspaceEntrySection title="Headers" initialOpen={false}>
+                <div
+                  class="grid grid-cols-[2%_1fr_1fr_2%] items-center justify-center gap-2 text-sm"
+                >
+                  {#each _state.entry.headers as header (header.id)}
+                    <Checkbox
+                      checked={header.enabled}
+                      onCheckedChange={() =>
+                        updateHeaderEnabled(header.id, !header.enabled)}
+                    />
 
-                  <Trash
-                    class="col-start-4 h-4 w-4 cursor-pointer text-muted-foreground hover:text-destructive"
-                    onclick={() => deleteHeader(header.id)}
-                  />
-                {/each}
+                    <Input
+                      id={`${header.id}_header_key`}
+                      class="col-start-2"
+                      bind:value={header.name}
+                      placeholder="Name"
+                      oninput={() =>
+                        updateHeader(header.id, header.name, header.value)}
+                    />
 
-                <PlusIcon
-                  class="border p-1 rounded-2xl mx-auto col-span-3 cursor-pointer"
-                  onclick={() => insertHeader()}
-                />
-              </div>
-            </div>
+                    <Input
+                      id={`${header.id}_header_val`}
+                      class="col-start-3"
+                      bind:value={header.value}
+                      placeholder="Value"
+                      oninput={() =>
+                        updateHeader(header.id, header.name, header.value)}
+                    />
+
+                    <Trash
+                      class="col-start-4 h-4 w-4 cursor-pointer text-muted-foreground hover:text-destructive"
+                      onclick={() => deleteHeader(header.id)}
+                    />
+                  {/each}
+
+                  <KeyValInput
+                    class="col-start-2 col-span-2"
+                    onInput={handleAddHeader}
+                  />
+                </div>
+              </WorkspaceEntrySection>
 
-            <Tabs.Content value="params" class="space-y-4">
               <!-- ================= PATH ================= -->
 
               {#if _state.entry?.path?.length > 0}
-                <div>
-                  <h3
-                    class="mb-2 pb-1 pointer-events-none text-xs font-medium border-b border-secondary text-muted-foreground"
-                  >
-                    Path
-                  </h3>
+                <WorkspaceEntrySection title="Path">
                   <div class="grid grid-cols-2 gap-2 text-sm">
                     {#each _state.entry.path as param}
                       <Input
@@ -419,78 +421,67 @@
                       />
                     {/each}
                   </div>
-                </div>
+                </WorkspaceEntrySection>
               {/if}
 
               <!-- ================= QUERY ================= -->
 
-              <div>
-                <h3
-                  class="mb-2 pb-1 pointer-events-none text-xs font-medium border-b border-secondary text-muted-foreground"
-                >
-                  Query
-                </h3>
-                <div
-                  class="grid grid-cols-[2%_1fr_1fr_2%] items-center justify-center gap-2 text-sm"
-                >
-                  {#each _state.entry.query as param (param.id)}
-                    <div class="flex justify-center">
-                      <Checkbox
-                        checked={param.position != null}
-                        onCheckedChange={() => updateQueryParamEnabled(param)}
+              {#if _state.entry?.query?.length > 0}
+                <WorkspaceEntrySection title="Query">
+                  <div
+                    class="grid grid-cols-[2%_1fr_1fr_2%] items-center justify-center gap-2 text-sm"
+                  >
+                    {#each _state.entry.query as param (param.id)}
+                      <div class="flex justify-center">
+                        <Checkbox
+                          checked={param.position != null}
+                          onCheckedChange={() => updateQueryParamEnabled(param)}
+                        />
+                      </div>
+                      <Input
+                        id={`${param.id}_query_key`}
+                        class={param.position == null
+                          ? `text-muted-foreground opacity-75`
+                          : ""}
+                        bind:value={param.key}
+                        placeholder="key"
+                        oninput={() =>
+                          handleUrlUpdate({
+                            type: "Query",
+                            url: _state.entry.url,
+                            param,
+                          })}
                       />
-                    </div>
-                    <Input
-                      id={`${param.id}_query_key`}
-                      class={param.position == null
-                        ? `text-muted-foreground opacity-75`
-                        : ""}
-                      bind:value={param.key}
-                      placeholder="key"
-                      oninput={() =>
-                        handleUrlUpdate({
-                          type: "Query",
-                          url: _state.entry.url,
-                          param,
-                        })}
-                    />
-                    <Input
-                      id={`${param.id}_query_val`}
-                      class={param.position == null
-                        ? `text-muted-foreground opacity-75`
-                        : ""}
-                      bind:value={param.value}
-                      placeholder="value"
-                      oninput={() =>
-                        handleUrlUpdate({
-                          type: "Query",
-                          url: _state.entry.url,
-                          param,
-                        })}
-                    />
-                    <div class="flex justify-center">
-                      <Trash
-                        class="h-4 w-4 cursor-pointer text-muted-foreground hover:text-destructive"
-                        onclick={() => deleteQueryParam(param)}
+                      <Input
+                        id={`${param.id}_query_val`}
+                        class={param.position == null
+                          ? `text-muted-foreground opacity-75`
+                          : ""}
+                        bind:value={param.value}
+                        placeholder="value"
+                        oninput={() =>
+                          handleUrlUpdate({
+                            type: "Query",
+                            url: _state.entry.url,
+                            param,
+                          })}
                       />
-                    </div>
-                  {/each}
-                  <!-- ================= ADD QUERY PARAM ================= -->
+                      <div class="flex justify-center">
+                        <Trash
+                          class="h-4 w-4 cursor-pointer text-muted-foreground hover:text-destructive"
+                          onclick={() => deleteQueryParam(param)}
+                        />
+                      </div>
+                    {/each}
+                    <!-- ================= ADD QUERY PARAM ================= -->
 
-                  <Input
-                    class="col-start-2"
-                    bind:value={addQueryParamKeyInput}
-                    placeholder="Key"
-                    oninput={() => handleAddQueryParam()}
-                  />
-                  <Input
-                    class="col-start-3"
-                    bind:value={addQueryParamValInput}
-                    placeholder="Value"
-                    oninput={() => handleAddQueryParam()}
-                  />
-                </div>
-              </div>
+                    <KeyValInput
+                      class="col-start-2 col-span-2"
+                      onInput={handleAddQueryParam}
+                    />
+                  </div>
+                </WorkspaceEntrySection>
+              {/if}
 
               <!-- ================= BODY ================= -->
 

+ 36 - 0
src/lib/components/WorkspaceEntrySection.svelte

@@ -0,0 +1,36 @@
+<script lang="ts">
+  import { ChevronDown, ChevronRight } from "@lucide/svelte";
+
+  type Props = {
+    title: string;
+    children: any;
+    initialOpen?: boolean;
+  };
+
+  let { title, children, initialOpen = true }: Props = $props();
+
+  let open = $state(initialOpen);
+</script>
+
+<div>
+  <h3 class="mb-2 pb-1 border-b border-secondary">
+    <button
+      type="button"
+      onclick={() => (open = !open)}
+      aria-expanded={open}
+      class="flex w-full cursor-pointer gap-1 items-center text-xs font-medium text-muted-foreground"
+    >
+      {#if open}
+        <ChevronDown size={14} />
+      {:else}
+        <ChevronRight size={14} />
+      {/if}
+
+      <span>{title}</span>
+    </button>
+  </h3>
+
+  {#if open}
+    {@render children?.()}
+  {/if}
+</div>

+ 4 - 0
src/lib/settings.svelte.ts

@@ -23,6 +23,10 @@ export async function init() {
 
 export type Settings = {
   theme: "dark" | "light";
+
+  /**
+   * Last selected workspace entry, loaded on init.
+   */
   lastEntry: WorkspaceEntry | null;
 
   /**

+ 11 - 4
src/lib/state.svelte.ts

@@ -15,7 +15,6 @@ import type {
   ResponseResult,
   QueryParam,
 } from "./types";
-import * as Resizable from "$lib/components/ui/resizable/index";
 import { getSetting, setSetting } from "./settings.svelte";
 
 export type WorkspaceState = {
@@ -720,13 +719,15 @@ export async function deleteEnvVariable(id: number) {
   );
 }
 
-export async function insertHeader() {
-  const header = await invoke("insert_header", {
+export async function insertHeader(name: string = "", value: string = "") {
+  const header: RequestHeader = await invoke("insert_header", {
     entryId: state.entry!!.id,
-    insert: { name: "", value: "" },
+    insert: { name, value },
   });
 
   state.entry!!.headers.push(header);
+
+  return header.id;
 }
 
 export async function updateHeader(id: number, name: string, value: string) {
@@ -738,6 +739,12 @@ export async function updateHeader(id: number, name: string, value: string) {
   header.value = value;
 }
 
+export async function updateHeaderEnabled(id: number, enabled: boolean) {
+  await invoke("update_header_enabled", { headerId: id, enabled });
+  const header = state.entry!!.headers.find((header) => header.id === id);
+  header.enabled = enabled;
+}
+
 export async function deleteHeader(id: number) {
   await invoke("delete_header", {
     headerId: id,

+ 1 - 0
src/lib/types.ts

@@ -62,6 +62,7 @@ export type RequestHeader = {
   id: number;
   name: string;
   value: string;
+  enabled: boolean;
 };
 
 export type PathParam = {