mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-18 06:49:50 +02:00
GraphQL autocomplete and duplicate request
This commit is contained in:
@@ -58,25 +58,24 @@ async fn migrate_db(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn send_request(
|
async fn send_ephemeral_request(
|
||||||
|
request: models::HttpRequest,
|
||||||
app_handle: AppHandle<Wry>,
|
app_handle: AppHandle<Wry>,
|
||||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
request_id: &str,
|
) -> Result<models::HttpResponse, String> {
|
||||||
) -> Result<String, String> {
|
|
||||||
let pool = &*db_instance.lock().await;
|
let pool = &*db_instance.lock().await;
|
||||||
|
let response = models::HttpResponse::default();
|
||||||
|
return actually_send_ephemeral_request(request, response, app_handle, pool).await;
|
||||||
|
}
|
||||||
|
|
||||||
let req = models::get_request(request_id, pool)
|
async fn actually_send_ephemeral_request(
|
||||||
.await
|
request: models::HttpRequest,
|
||||||
.expect("Failed to get request");
|
mut response: models::HttpResponse,
|
||||||
|
app_handle: AppHandle<Wry>,
|
||||||
let mut response = models::create_response(&req.id, 0, "", 0, None, "", vec![], pool)
|
pool: &Pool<Sqlite>,
|
||||||
.await
|
) -> Result<models::HttpResponse, String> {
|
||||||
.expect("Failed to create response");
|
|
||||||
app_handle.emit_all("updated_response", &response).unwrap();
|
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
let mut url_string = request.url.to_string();
|
||||||
let mut url_string = req.url.to_string();
|
|
||||||
|
|
||||||
let mut variables = HashMap::new();
|
let mut variables = HashMap::new();
|
||||||
variables.insert("PROJECT_ID", "project_123");
|
variables.insert("PROJECT_ID", "project_123");
|
||||||
@@ -108,7 +107,7 @@ async fn send_request(
|
|||||||
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
|
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
|
||||||
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
|
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
|
||||||
|
|
||||||
for h in req.headers.0 {
|
for h in request.headers.0 {
|
||||||
if h.name.is_empty() && h.value.is_empty() {
|
if h.name.is_empty() && h.value.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -133,10 +132,10 @@ async fn send_request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let m =
|
let m =
|
||||||
Method::from_bytes(req.method.to_uppercase().as_bytes()).expect("Failed to create method");
|
Method::from_bytes(request.method.to_uppercase().as_bytes()).expect("Failed to create method");
|
||||||
let builder = client.request(m, url_string.to_string()).headers(headers);
|
let builder = client.request(m, url_string.to_string()).headers(headers);
|
||||||
|
|
||||||
let sendable_req_result = match (req.body, req.body_type) {
|
let sendable_req_result = match (request.body, request.body_type) {
|
||||||
(Some(b), Some(_)) => builder.body(b).build(),
|
(Some(b), Some(_)) => builder.body(b).build(),
|
||||||
_ => builder.build(),
|
_ => builder.build(),
|
||||||
};
|
};
|
||||||
@@ -173,28 +172,49 @@ async fn send_request(
|
|||||||
response.url = v.url().to_string();
|
response.url = v.url().to_string();
|
||||||
response.body = v.text().await.expect("Failed to get body");
|
response.body = v.text().await.expect("Failed to get body");
|
||||||
response.elapsed = start.elapsed().as_millis() as i64;
|
response.elapsed = start.elapsed().as_millis() as i64;
|
||||||
response = models::update_response(response, pool)
|
response = models::update_response_if_id(response, pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to update response");
|
.expect("Failed to update response");
|
||||||
app_handle.emit_all("updated_response", &response).unwrap();
|
app_handle.emit_all("updated_response", &response).unwrap();
|
||||||
Ok(response.id)
|
Ok(response)
|
||||||
}
|
}
|
||||||
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn send_request(
|
||||||
|
app_handle: AppHandle<Wry>,
|
||||||
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
|
request_id: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let pool = &*db_instance.lock().await;
|
||||||
|
|
||||||
|
let req = models::get_request(request_id, pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get request");
|
||||||
|
|
||||||
|
let response = models::create_response(&req.id, 0, "", 0, None, "", vec![], pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create response");
|
||||||
|
app_handle.emit_all("updated_response", &response).unwrap();
|
||||||
|
|
||||||
|
actually_send_ephemeral_request(req, response, app_handle, pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn response_err(
|
async fn response_err(
|
||||||
mut response: models::HttpResponse,
|
mut response: models::HttpResponse,
|
||||||
error: String,
|
error: String,
|
||||||
app_handle: AppHandle<Wry>,
|
app_handle: AppHandle<Wry>,
|
||||||
pool: &Pool<Sqlite>,
|
pool: &Pool<Sqlite>,
|
||||||
) -> Result<String, String> {
|
) -> Result<models::HttpResponse, String> {
|
||||||
response.error = Some(error.clone());
|
response.error = Some(error.clone());
|
||||||
response = models::update_response(response, pool)
|
response = models::update_response_if_id(response, pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to update response");
|
.expect("Failed to update response");
|
||||||
app_handle.emit_all("updated_response", &response).unwrap();
|
app_handle.emit_all("updated_response", &response).unwrap();
|
||||||
Ok(response.id)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -268,6 +288,20 @@ async fn create_request(
|
|||||||
Ok(created_request.id)
|
Ok(created_request.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn duplicate_request(
|
||||||
|
id: &str,
|
||||||
|
app_handle: AppHandle<Wry>,
|
||||||
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let pool = &*db_instance.lock().await;
|
||||||
|
let request = models::duplicate_request(id, pool).await.expect("Failed to duplicate request");
|
||||||
|
app_handle
|
||||||
|
.emit_all("updated_request", &request)
|
||||||
|
.unwrap();
|
||||||
|
Ok(request.id)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn update_request(
|
async fn update_request(
|
||||||
request: models::HttpRequest,
|
request: models::HttpRequest,
|
||||||
@@ -458,7 +492,6 @@ fn main() {
|
|||||||
let p = dir.join("db.sqlite");
|
let p = dir.join("db.sqlite");
|
||||||
let p_string = p.to_string_lossy().replace(' ', "%20");
|
let p_string = p.to_string_lossy().replace(' ', "%20");
|
||||||
let url = format!("sqlite://{}?mode=rwc", p_string);
|
let url = format!("sqlite://{}?mode=rwc", p_string);
|
||||||
println!("DB PATH: {}", p_string);
|
|
||||||
tauri::async_runtime::block_on(async move {
|
tauri::async_runtime::block_on(async move {
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.connect(url.as_str())
|
.connect(url.as_str())
|
||||||
@@ -501,7 +534,7 @@ fn main() {
|
|||||||
} else {
|
} else {
|
||||||
event.window().open_devtools();
|
event.window().open_devtools();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -525,6 +558,8 @@ fn main() {
|
|||||||
get_request,
|
get_request,
|
||||||
requests,
|
requests,
|
||||||
send_request,
|
send_request,
|
||||||
|
send_ephemeral_request,
|
||||||
|
duplicate_request,
|
||||||
create_request,
|
create_request,
|
||||||
create_workspace,
|
create_workspace,
|
||||||
delete_workspace,
|
delete_workspace,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub struct HttpResponseHeader {
|
|||||||
pub value: String,
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct HttpResponse {
|
pub struct HttpResponse {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -180,6 +180,35 @@ pub async fn create_workspace(
|
|||||||
get_workspace(&id, pool).await
|
get_workspace(&id, pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn duplicate_request(
|
||||||
|
id: &str,
|
||||||
|
pool: &Pool<Sqlite>,
|
||||||
|
) -> Result<HttpRequest, sqlx::Error> {
|
||||||
|
let existing = get_request(id, pool).await.expect("Failed to get request to duplicate");
|
||||||
|
// TODO: Figure out how to make this better
|
||||||
|
let b2;
|
||||||
|
let body = match existing.body {
|
||||||
|
Some(b) => {
|
||||||
|
b2 = b;
|
||||||
|
Some(b2.as_str())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
upsert_request(
|
||||||
|
None,
|
||||||
|
existing.workspace_id.as_str(),
|
||||||
|
existing.name.as_str(),
|
||||||
|
existing.method.as_str(),
|
||||||
|
body,
|
||||||
|
existing.body_type,
|
||||||
|
existing.url.as_str(),
|
||||||
|
existing.headers.0,
|
||||||
|
existing.sort_priority,
|
||||||
|
pool,
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn upsert_request(
|
pub async fn upsert_request(
|
||||||
id: Option<&str>,
|
id: Option<&str>,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
@@ -350,6 +379,16 @@ pub async fn create_response(
|
|||||||
get_response(&id, pool).await
|
get_response(&id, pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_response_if_id(
|
||||||
|
response: HttpResponse,
|
||||||
|
pool: &Pool<Sqlite>,
|
||||||
|
) -> Result<HttpResponse, sqlx::Error> {
|
||||||
|
if response.id == "" {
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
return update_response(response, pool).await;
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update_response(
|
pub async fn update_response(
|
||||||
response: HttpResponse,
|
response: HttpResponse,
|
||||||
pool: &Pool<Sqlite>,
|
pool: &Pool<Sqlite>,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
|||||||
import { DEFAULT_FONT_SIZE } from '../lib/constants';
|
import { DEFAULT_FONT_SIZE } from '../lib/constants';
|
||||||
import { extractKeyValue } from '../lib/keyValueStore';
|
import { extractKeyValue } from '../lib/keyValueStore';
|
||||||
import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models';
|
import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models';
|
||||||
import { convertDates } from '../lib/models';
|
|
||||||
import { AppRouter } from './AppRouter';
|
import { AppRouter } from './AppRouter';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -52,13 +51,13 @@ await listen('updated_request', ({ payload: request }: { payload: HttpRequest })
|
|||||||
for (const r of requests) {
|
for (const r of requests) {
|
||||||
if (r.id === request.id) {
|
if (r.id === request.id) {
|
||||||
found = true;
|
found = true;
|
||||||
newRequests.push(convertDates(request));
|
newRequests.push(request);
|
||||||
} else {
|
} else {
|
||||||
newRequests.push(r);
|
newRequests.push(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
newRequests.push(convertDates(request));
|
newRequests.push(request);
|
||||||
}
|
}
|
||||||
return newRequests;
|
return newRequests;
|
||||||
},
|
},
|
||||||
@@ -74,13 +73,13 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse
|
|||||||
for (const r of responses) {
|
for (const r of responses) {
|
||||||
if (r.id === response.id) {
|
if (r.id === response.id) {
|
||||||
found = true;
|
found = true;
|
||||||
newResponses.push(convertDates(response));
|
newResponses.push(response);
|
||||||
} else {
|
} else {
|
||||||
newResponses.push(r);
|
newResponses.push(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
newResponses.push(convertDates(response));
|
newResponses.push(response);
|
||||||
}
|
}
|
||||||
return newResponses;
|
return newResponses;
|
||||||
},
|
},
|
||||||
@@ -94,13 +93,13 @@ await listen('updated_workspace', ({ payload: workspace }: { payload: Workspace
|
|||||||
for (const w of workspaces) {
|
for (const w of workspaces) {
|
||||||
if (w.id === workspace.id) {
|
if (w.id === workspace.id) {
|
||||||
found = true;
|
found = true;
|
||||||
newWorkspaces.push(convertDates(workspace));
|
newWorkspaces.push(workspace);
|
||||||
} else {
|
} else {
|
||||||
newWorkspaces.push(w);
|
newWorkspaces.push(w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
newWorkspaces.push(convertDates(workspace));
|
newWorkspaces.push(workspace);
|
||||||
}
|
}
|
||||||
return newWorkspaces;
|
return newWorkspaces;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import type { Extension } from '@codemirror/state';
|
||||||
|
import { graphql } from 'cm6-graphql';
|
||||||
import { formatSdl } from 'format-graphql';
|
import { formatSdl } from 'format-graphql';
|
||||||
import { useMemo } from 'react';
|
import { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useUniqueKey } from '../hooks/useUniqueKey';
|
import { useUniqueKey } from '../hooks/useUniqueKey';
|
||||||
import { Separator } from './core/Separator';
|
import type { HttpRequest } from '../lib/models';
|
||||||
|
import { sendEphemeralRequest } from '../lib/sendEphemeralRequest';
|
||||||
import type { EditorProps } from './core/Editor';
|
import type { EditorProps } from './core/Editor';
|
||||||
import { Editor } from './core/Editor';
|
import { Editor } from './core/Editor';
|
||||||
|
import { Separator } from './core/Separator';
|
||||||
|
|
||||||
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'defaultValue' | 'className'>;
|
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'defaultValue' | 'className'> & {
|
||||||
|
baseRequest: HttpRequest;
|
||||||
|
};
|
||||||
|
|
||||||
interface GraphQLBody {
|
interface GraphQLBody {
|
||||||
query: string;
|
query: string;
|
||||||
@@ -13,7 +20,7 @@ interface GraphQLBody {
|
|||||||
operationName?: string;
|
operationName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GraphQLEditor({ defaultValue, onChange, ...extraEditorProps }: Props) {
|
export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEditorProps }: Props) {
|
||||||
const queryKey = useUniqueKey();
|
const queryKey = useUniqueKey();
|
||||||
const { query, variables } = useMemo<GraphQLBody>(() => {
|
const { query, variables } = useMemo<GraphQLBody>(() => {
|
||||||
if (!defaultValue) {
|
if (!defaultValue) {
|
||||||
@@ -46,12 +53,29 @@ export function GraphQLEditor({ defaultValue, onChange, ...extraEditorProps }: P
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [graphqlExtension, setGraphqlExtension] = useState<Extension>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
query: getIntrospectionQuery(),
|
||||||
|
operationName: 'IntrospectionQuery',
|
||||||
|
});
|
||||||
|
const req: HttpRequest = { ...baseRequest, body, id: '' };
|
||||||
|
sendEphemeralRequest(req).then((response) => {
|
||||||
|
console.log('RESPONSE', response.body);
|
||||||
|
const { data } = JSON.parse(response.body);
|
||||||
|
const schema = buildClientSchema(data);
|
||||||
|
setGraphqlExtension(graphql(schema, {}));
|
||||||
|
});
|
||||||
|
}, [baseRequest.url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-2 h-full grid grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
<div className="pb-2 h-full grid grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
||||||
<Editor
|
<Editor
|
||||||
key={queryKey.key}
|
key={queryKey.key}
|
||||||
heightMode="auto"
|
heightMode="auto"
|
||||||
defaultValue={query ?? ''}
|
defaultValue={query ?? ''}
|
||||||
|
languageExtension={graphqlExtension}
|
||||||
onChange={handleChangeQuery}
|
onChange={handleChangeQuery}
|
||||||
contentType="application/graphql"
|
contentType="application/graphql"
|
||||||
placeholder="..."
|
placeholder="..."
|
||||||
@@ -59,7 +83,6 @@ export function GraphQLEditor({ defaultValue, onChange, ...extraEditorProps }: P
|
|||||||
{...extraEditorProps}
|
{...extraEditorProps}
|
||||||
/>
|
/>
|
||||||
<Separator variant="primary" />
|
<Separator variant="primary" />
|
||||||
{/*<Separator variant="secondary" />*/}
|
|
||||||
<p className="pt-1 text-gray-500 text-sm">Variables</p>
|
<p className="pt-1 text-gray-500 text-sm">Variables</p>
|
||||||
<Editor
|
<Editor
|
||||||
useTemplating
|
useTemplating
|
||||||
|
|||||||
@@ -39,17 +39,21 @@ const headerOptionsMap: Record<string, string[]> = {
|
|||||||
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {
|
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {
|
||||||
const name = headerName.toLowerCase().trim();
|
const name = headerName.toLowerCase().trim();
|
||||||
const options: GenericCompletionConfig['options'] =
|
const options: GenericCompletionConfig['options'] =
|
||||||
headerOptionsMap[name]?.map((o, i) => ({
|
headerOptionsMap[name]?.map((o) => ({
|
||||||
label: o,
|
label: o,
|
||||||
type: 'constant',
|
type: 'constant',
|
||||||
boost: 99 - i, // Max boost is 99
|
boost: 1, // Put above other completions
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
return { minMatch: MIN_MATCH, options };
|
return { minMatch: MIN_MATCH, options };
|
||||||
};
|
};
|
||||||
|
|
||||||
const nameAutocomplete: PairEditorProps['nameAutocomplete'] = {
|
const nameAutocomplete: PairEditorProps['nameAutocomplete'] = {
|
||||||
minMatch: MIN_MATCH,
|
minMatch: MIN_MATCH,
|
||||||
options: headerNames.map((t, i) => ({ label: t, type: 'constant', boost: 99 - i })),
|
options: headerNames.map((t) => ({
|
||||||
|
label: t,
|
||||||
|
type: 'constant',
|
||||||
|
boost: 1, // Put above other completions
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateHttpHeader = (v: string) => {
|
const validateHttpHeader = (v: string) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||||
@@ -23,6 +23,7 @@ export function RequestPane({ fullHeight, className }: Props) {
|
|||||||
const activeRequest = useActiveRequest();
|
const activeRequest = useActiveRequest();
|
||||||
const activeRequestId = activeRequest?.id ?? null;
|
const activeRequestId = activeRequest?.id ?? null;
|
||||||
const updateRequest = useUpdateRequest(activeRequestId);
|
const updateRequest = useUpdateRequest(activeRequestId);
|
||||||
|
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
||||||
const activeTab = useKeyValue<string>({
|
const activeTab = useKeyValue<string>({
|
||||||
key: ['active_request_body_tab'],
|
key: ['active_request_body_tab'],
|
||||||
defaultValue: 'body',
|
defaultValue: 'body',
|
||||||
@@ -34,12 +35,24 @@ export function RequestPane({ fullHeight, className }: Props) {
|
|||||||
value: 'body',
|
value: 'body',
|
||||||
label: activeRequest?.bodyType ?? 'No Body',
|
label: activeRequest?.bodyType ?? 'No Body',
|
||||||
options: {
|
options: {
|
||||||
onChange: (bodyType: HttpRequest['bodyType']) => {
|
onChange: async (bodyType: HttpRequest['bodyType']) => {
|
||||||
const patch: Partial<HttpRequest> = { bodyType };
|
const patch: Partial<HttpRequest> = { bodyType };
|
||||||
if (bodyType == HttpRequestBodyType.GraphQL) {
|
if (bodyType == HttpRequestBodyType.GraphQL) {
|
||||||
patch.method = 'POST';
|
patch.method = 'POST';
|
||||||
|
patch.headers = [
|
||||||
|
...(activeRequest?.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
|
||||||
|
[]),
|
||||||
|
{
|
||||||
|
name: 'Content-Type',
|
||||||
|
value: 'application/json',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setTimeout(() => {
|
||||||
|
setForceUpdateHeaderEditorKey((u) => u + 1);
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
updateRequest.mutate(patch);
|
await updateRequest.mutate(patch);
|
||||||
},
|
},
|
||||||
value: activeRequest?.bodyType ?? null,
|
value: activeRequest?.bodyType ?? null,
|
||||||
items: [
|
items: [
|
||||||
@@ -54,7 +67,7 @@ export function RequestPane({ fullHeight, className }: Props) {
|
|||||||
{ value: 'headers', label: 'Headers' },
|
{ value: 'headers', label: 'Headers' },
|
||||||
{ value: 'auth', label: 'Auth' },
|
{ value: 'auth', label: 'Auth' },
|
||||||
],
|
],
|
||||||
[activeRequest?.bodyType ?? 'n/a'],
|
[activeRequest?.bodyType, activeRequest?.headers],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBodyChange = useCallback((body: string) => updateRequest.mutate({ body }), []);
|
const handleBodyChange = useCallback((body: string) => updateRequest.mutate({ body }), []);
|
||||||
@@ -88,7 +101,7 @@ export function RequestPane({ fullHeight, className }: Props) {
|
|||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value="headers">
|
<TabContent value="headers">
|
||||||
<HeaderEditor
|
<HeaderEditor
|
||||||
key={activeRequestId}
|
key={`${activeRequest.id}::${forceUpdateHeaderEditorKey}`}
|
||||||
headers={activeRequest.headers}
|
headers={activeRequest.headers}
|
||||||
onChange={handleHeadersChange}
|
onChange={handleHeadersChange}
|
||||||
/>
|
/>
|
||||||
@@ -123,6 +136,7 @@ export function RequestPane({ fullHeight, className }: Props) {
|
|||||||
) : activeRequest.bodyType === HttpRequestBodyType.GraphQL ? (
|
) : activeRequest.bodyType === HttpRequestBodyType.GraphQL ? (
|
||||||
<GraphQLEditor
|
<GraphQLEditor
|
||||||
key={activeRequest.id}
|
key={activeRequest.id}
|
||||||
|
baseRequest={activeRequest}
|
||||||
className="!bg-gray-50"
|
className="!bg-gray-50"
|
||||||
defaultValue={activeRequest?.body ?? ''}
|
defaultValue={activeRequest?.body ?? ''}
|
||||||
onChange={handleBodyChange}
|
onChange={handleBodyChange}
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
import type { HTMLAttributes, ReactElement } from 'react';
|
||||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||||
|
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||||
import { Dropdown } from './core/Dropdown';
|
import { Dropdown } from './core/Dropdown';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
requestId: string;
|
||||||
|
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RequestSettingsDropdown({ className }: Props) {
|
export function RequestSettingsDropdown({ requestId, children }: Props) {
|
||||||
const activeRequestId = useActiveRequestId();
|
const deleteRequest = useDeleteRequest(requestId ?? null);
|
||||||
const deleteRequest = useDeleteRequest(activeRequestId ?? null);
|
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: 'Something Else',
|
label: 'Duplicate',
|
||||||
onSelect: () => null,
|
onSelect: duplicateRequest.mutate,
|
||||||
leftSlot: <Icon icon="camera" />,
|
leftSlot: <Icon icon="copy" />,
|
||||||
},
|
},
|
||||||
'-----',
|
|
||||||
{
|
{
|
||||||
label: 'Delete Request',
|
label: 'Delete',
|
||||||
onSelect: deleteRequest.mutate,
|
onSelect: deleteRequest.mutate,
|
||||||
leftSlot: <Icon icon="trash" />,
|
leftSlot: <Icon icon="trash" />,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<IconButton className={className} size="sm" title="Request Options" icon="gear" />
|
{children}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,17 @@ import type { XYCoord } from 'react-dnd';
|
|||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
|
||||||
import { useRequests } from '../hooks/useRequests';
|
import { useRequests } from '../hooks/useRequests';
|
||||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||||
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
|
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
|
||||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { Dropdown } from './core/Dropdown';
|
|
||||||
import { Icon } from './core/Icon';
|
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { HStack, VStack } from './core/Stacks';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
import { WindowDragRegion } from './core/WindowDragRegion';
|
import { WindowDragRegion } from './core/WindowDragRegion';
|
||||||
import { DropMarker } from './DropMarker';
|
import { DropMarker } from './DropMarker';
|
||||||
|
import { RequestSettingsDropdown } from './RequestSettingsDropdown';
|
||||||
import { ToggleThemeButton } from './ToggleThemeButton';
|
import { ToggleThemeButton } from './ToggleThemeButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -204,7 +202,6 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
|||||||
{ className, requestName, requestId, workspaceId, active, sidebarWidth }: SidebarItemProps,
|
{ className, requestName, requestId, workspaceId, active, sidebarWidth }: SidebarItemProps,
|
||||||
ref: ForwardedRef<HTMLLIElement>,
|
ref: ForwardedRef<HTMLLIElement>,
|
||||||
) {
|
) {
|
||||||
const deleteRequest = useDeleteRequest(requestId);
|
|
||||||
const updateRequest = useUpdateRequest(requestId);
|
const updateRequest = useUpdateRequest(requestId);
|
||||||
const [editing, setEditing] = useState<boolean>(false);
|
const [editing, setEditing] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -244,17 +241,6 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
|||||||
[active],
|
[active],
|
||||||
);
|
);
|
||||||
|
|
||||||
const actionItems = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
label: 'Delete Request',
|
|
||||||
onSelect: deleteRequest.mutate,
|
|
||||||
leftSlot: <Icon icon="trash" />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -295,19 +281,18 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Dropdown items={actionItems}>
|
<RequestSettingsDropdown requestId={requestId}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
color="custom"
|
||||||
|
size="sm"
|
||||||
|
title="Request Options"
|
||||||
|
icon="dotsH"
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'absolute right-0 top-0 transition-opacity opacity-0',
|
'absolute right-0 top-0 transition-opacity opacity-0',
|
||||||
'group-hover/item:opacity-100 focus-visible:opacity-100',
|
'group-hover/item:opacity-100 focus-visible:opacity-100',
|
||||||
)}
|
)}
|
||||||
color="custom"
|
|
||||||
size="sm"
|
|
||||||
iconSize="sm"
|
|
||||||
title="Delete request"
|
|
||||||
icon="dotsH"
|
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</RequestSettingsDropdown>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
|||||||
className="px-0"
|
className="px-0"
|
||||||
name="url"
|
name="url"
|
||||||
label="Enter URL"
|
label="Enter URL"
|
||||||
containerClassName="shadow shadow-gray-100 dark:shadow-gray-0"
|
containerClassName="shadow shadow-gray-100 dark:shadow-gray-50"
|
||||||
onChange={handleUrlChange}
|
onChange={handleUrlChange}
|
||||||
defaultValue={url}
|
defaultValue={url}
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
|
|||||||
@@ -50,7 +50,14 @@ export default function Workspace() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
||||||
<IconButton size="sm" title="" icon="magnifyingGlass" />
|
<IconButton size="sm" title="" icon="magnifyingGlass" />
|
||||||
<RequestSettingsDropdown className="pointer-events-auto" />
|
<RequestSettingsDropdown>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
title="Request Options"
|
||||||
|
icon="gear"
|
||||||
|
className="pointer-events-auto"
|
||||||
|
/>
|
||||||
|
</RequestSettingsDropdown>
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
>
|
>
|
||||||
{containerStyles && (
|
{containerStyles && (
|
||||||
<VStack
|
<VStack
|
||||||
|
space={0.5}
|
||||||
ref={initMenu}
|
ref={initMenu}
|
||||||
style={menuStyles}
|
style={menuStyles}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|||||||
@@ -151,7 +151,15 @@
|
|||||||
|
|
||||||
/* NOTE: Extra selector required to override default styles */
|
/* NOTE: Extra selector required to override default styles */
|
||||||
.cm-tooltip.cm-tooltip {
|
.cm-tooltip.cm-tooltip {
|
||||||
@apply shadow-lg bg-gray-50 rounded overflow-hidden text-gray-900 border border-gray-200 z-50 pointer-events-auto;
|
@apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-200 z-50 pointer-events-auto text-sm;
|
||||||
|
|
||||||
|
&.cm-completionInfo-right {
|
||||||
|
@apply ml-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cm-completionInfo-right-narrow {
|
||||||
|
@apply ml-1;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@apply transition-none;
|
@apply transition-none;
|
||||||
@@ -159,7 +167,7 @@
|
|||||||
|
|
||||||
&.cm-tooltip-autocomplete {
|
&.cm-tooltip-autocomplete {
|
||||||
& > ul {
|
& > ul {
|
||||||
@apply p-1 max-h-[20rem];
|
@apply p-1 max-h-[40vh];
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul > li {
|
& > ul > li {
|
||||||
@@ -177,5 +185,18 @@
|
|||||||
.cm-completionIcon {
|
.cm-completionIcon {
|
||||||
@apply text-sm flex items-center pb-0.5;
|
@apply text-sm flex items-center pb-0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.cm-completionLabel {
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-completionDetail {
|
||||||
|
@apply ml-auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add default icon. Needs low priority so it can be overwritten */
|
||||||
|
.cm-completionIcon::after {
|
||||||
|
content: '𝑥';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
|
import type { Extension } from '@codemirror/state';
|
||||||
import { Compartment, EditorState } from '@codemirror/state';
|
import { Compartment, EditorState } from '@codemirror/state';
|
||||||
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { EditorView } from 'codemirror';
|
import { EditorView } from 'codemirror';
|
||||||
import type { MutableRefObject } from 'react';
|
import type { MutableRefObject } from 'react';
|
||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
import { useUnmount } from 'react-use';
|
|
||||||
import { IconButton } from '../IconButton';
|
import { IconButton } from '../IconButton';
|
||||||
import './Editor.css';
|
import './Editor.css';
|
||||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||||
@@ -18,6 +18,7 @@ export interface _EditorProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
heightMode?: 'auto' | 'full';
|
heightMode?: 'auto' | 'full';
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
|
languageExtension?: Extension;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -38,6 +39,7 @@ export function _Editor({
|
|||||||
placeholder,
|
placeholder,
|
||||||
useTemplating,
|
useTemplating,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
languageExtension,
|
||||||
onChange,
|
onChange,
|
||||||
onFocus,
|
onFocus,
|
||||||
className,
|
className,
|
||||||
@@ -48,12 +50,6 @@ export function _Editor({
|
|||||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// Unmount the editor
|
|
||||||
useUnmount(() => {
|
|
||||||
cm.current?.view.destroy();
|
|
||||||
cm.current = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use ref so we can update the onChange handler without re-initializing the editor
|
// Use ref so we can update the onChange handler without re-initializing the editor
|
||||||
const handleChange = useRef<_EditorProps['onChange']>(onChange);
|
const handleChange = useRef<_EditorProps['onChange']>(onChange);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -87,9 +83,11 @@ export function _Editor({
|
|||||||
// Initialize the editor when ref mounts
|
// Initialize the editor when ref mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (wrapperRef.current === null || cm.current !== null) return;
|
if (wrapperRef.current === null || cm.current !== null) return;
|
||||||
|
let view: EditorView;
|
||||||
try {
|
try {
|
||||||
const languageCompartment = new Compartment();
|
const languageCompartment = new Compartment();
|
||||||
const langExt = getLanguageExtension({ contentType, useTemplating, autocomplete });
|
const langExt =
|
||||||
|
languageExtension ?? getLanguageExtension({ contentType, useTemplating, autocomplete });
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: `${defaultValue ?? ''}`,
|
doc: `${defaultValue ?? ''}`,
|
||||||
extensions: [
|
extensions: [
|
||||||
@@ -106,18 +104,18 @@ export function _Editor({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const view = new EditorView({ state, parent: wrapperRef.current });
|
view = new EditorView({ state, parent: wrapperRef.current });
|
||||||
cm.current = { view, languageCompartment };
|
cm.current = { view, languageCompartment };
|
||||||
|
syncGutterBg({ parent: wrapperRef.current, className });
|
||||||
if (autoFocus) view.focus();
|
if (autoFocus) view.focus();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Failed to initialize Codemirror', e);
|
console.log('Failed to initialize Codemirror', e);
|
||||||
}
|
}
|
||||||
}, [wrapperRef.current]);
|
return () => {
|
||||||
|
view.destroy();
|
||||||
useEffect(() => {
|
cm.current = null;
|
||||||
if (wrapperRef.current === null) return;
|
};
|
||||||
syncGutterBg({ parent: wrapperRef.current, className });
|
}, [wrapperRef.current, languageExtension]);
|
||||||
}, [className]);
|
|
||||||
|
|
||||||
const cmContainer = useMemo(
|
const cmContainer = useMemo(
|
||||||
() => (
|
() => (
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ import {
|
|||||||
rectangularSelection,
|
rectangularSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { tags as t } from '@lezer/highlight';
|
import { tags as t } from '@lezer/highlight';
|
||||||
import { graphqlLanguageSupport } from 'cm6-graphql';
|
import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
|
||||||
|
import { render } from 'react-dom';
|
||||||
import type { EditorProps } from './index';
|
import type { EditorProps } from './index';
|
||||||
import { text } from './text/extension';
|
import { text } from './text/extension';
|
||||||
import { twig } from './twig/extension';
|
import { twig } from './twig/extension';
|
||||||
@@ -97,6 +98,9 @@ export function getLanguageExtension({
|
|||||||
useTemplating = false,
|
useTemplating = false,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
}: Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
|
}: Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
|
||||||
|
if (contentType === 'application/graphql') {
|
||||||
|
return graphql();
|
||||||
|
}
|
||||||
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
|
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
|
||||||
const base = syntaxExtensions[justContentType] ?? text();
|
const base = syntaxExtensions[justContentType] ?? text();
|
||||||
if (!useTemplating) {
|
if (!useTemplating) {
|
||||||
@@ -115,7 +119,14 @@ export const baseExtensions = [
|
|||||||
// TODO: Figure out how to debounce showing of autocomplete in a good way
|
// TODO: Figure out how to debounce showing of autocomplete in a good way
|
||||||
// debouncedAutocompletionDisplay({ millis: 1000 }),
|
// debouncedAutocompletionDisplay({ millis: 1000 }),
|
||||||
// autocompletion({ closeOnBlur: true, interactionDelay: 200, activateOnTyping: false }),
|
// autocompletion({ closeOnBlur: true, interactionDelay: 200, activateOnTyping: false }),
|
||||||
autocompletion({ closeOnBlur: true, interactionDelay: 200 }),
|
autocompletion({
|
||||||
|
// closeOnBlur: false,
|
||||||
|
interactionDelay: 200,
|
||||||
|
compareCompletions: (a, b) => {
|
||||||
|
// Don't sort completions at all, only on boost
|
||||||
|
return (a.boost ?? 0) - (b.boost ?? 0);
|
||||||
|
},
|
||||||
|
}),
|
||||||
syntaxHighlighting(myHighlightStyle),
|
syntaxHighlighting(myHighlightStyle),
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ export function genericCompletion({ options, minMatch = 1 }: GenericCompletionCo
|
|||||||
if (!matchedMinimumLength && !context.explicit) return null;
|
if (!matchedMinimumLength && !context.explicit) return null;
|
||||||
|
|
||||||
const optionsWithoutExactMatches = options.filter((o) => o.label !== toMatch.text);
|
const optionsWithoutExactMatches = options.filter((o) => o.label !== toMatch.text);
|
||||||
return { from: toMatch.from, options: optionsWithoutExactMatches };
|
return { from: toMatch.from, options: optionsWithoutExactMatches, info: 'hello' };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ClockIcon,
|
ClockIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
ColorWheelIcon,
|
ColorWheelIcon,
|
||||||
|
CopyIcon,
|
||||||
Cross2Icon,
|
Cross2Icon,
|
||||||
DividerHorizontalIcon,
|
DividerHorizontalIcon,
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
@@ -40,8 +41,11 @@ const icons = {
|
|||||||
check: CheckIcon,
|
check: CheckIcon,
|
||||||
checkbox: CheckboxIcon,
|
checkbox: CheckboxIcon,
|
||||||
clock: ClockIcon,
|
clock: ClockIcon,
|
||||||
|
chevronDown: ChevronDownIcon,
|
||||||
code: CodeIcon,
|
code: CodeIcon,
|
||||||
colorWheel: ColorWheelIcon,
|
colorWheel: ColorWheelIcon,
|
||||||
|
copy: CopyIcon,
|
||||||
|
dividerH: DividerHorizontalIcon,
|
||||||
dotsH: DotsHorizontalIcon,
|
dotsH: DotsHorizontalIcon,
|
||||||
dotsV: DotsVerticalIcon,
|
dotsV: DotsVerticalIcon,
|
||||||
drag: DragHandleDots2Icon,
|
drag: DragHandleDots2Icon,
|
||||||
@@ -50,12 +54,10 @@ const icons = {
|
|||||||
home: HomeIcon,
|
home: HomeIcon,
|
||||||
listBullet: ListBulletIcon,
|
listBullet: ListBulletIcon,
|
||||||
magicWand: MagicWandIcon,
|
magicWand: MagicWandIcon,
|
||||||
chevronDown: ChevronDownIcon,
|
|
||||||
magnifyingGlass: MagnifyingGlassIcon,
|
magnifyingGlass: MagnifyingGlassIcon,
|
||||||
moon: MoonIcon,
|
moon: MoonIcon,
|
||||||
paperPlane: PaperPlaneIcon,
|
paperPlane: PaperPlaneIcon,
|
||||||
plus: PlusIcon,
|
plus: PlusIcon,
|
||||||
dividerH: DividerHorizontalIcon,
|
|
||||||
plusCircle: PlusCircledIcon,
|
plusCircle: PlusCircledIcon,
|
||||||
question: QuestionMarkIcon,
|
question: QuestionMarkIcon,
|
||||||
rows: RowsIcon,
|
rows: RowsIcon,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
'@container',
|
'@container',
|
||||||
'pb-2 grid',
|
'pb-2 grid',
|
||||||
// NOTE: Add padding to top so overflow doesn't hide drop marker
|
// NOTE: Add padding to top so overflow doesn't hide drop marker
|
||||||
'pt-1',
|
'pt-1 -my-1',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{pairs.map((p, i) => {
|
{pairs.map((p, i) => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { forwardRef } from 'react';
|
|||||||
|
|
||||||
const gapClasses = {
|
const gapClasses = {
|
||||||
0: 'gap-0',
|
0: 'gap-0',
|
||||||
|
0.5: 'gap-0.5',
|
||||||
1: 'gap-1',
|
1: 'gap-1',
|
||||||
2: 'gap-2',
|
2: 'gap-2',
|
||||||
3: 'gap-3',
|
3: 'gap-3',
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
|
import { useRoutes } from './useRoutes';
|
||||||
|
|
||||||
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
|
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
|
||||||
const workspace = useActiveWorkspace();
|
const workspace = useActiveWorkspace();
|
||||||
const navigate = useNavigate();
|
const routes = useRoutes();
|
||||||
|
|
||||||
return useMutation<string, unknown, Pick<HttpRequest, 'name' | 'sortPriority'>>({
|
return useMutation<string, unknown, Pick<HttpRequest, 'name' | 'sortPriority'>>({
|
||||||
mutationFn: (patch) => {
|
mutationFn: (patch) => {
|
||||||
@@ -16,8 +16,8 @@ export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean })
|
|||||||
return invoke('create_request', { ...patch, workspaceId: workspace.id });
|
return invoke('create_request', { ...patch, workspaceId: workspace.id });
|
||||||
},
|
},
|
||||||
onSuccess: async (requestId) => {
|
onSuccess: async (requestId) => {
|
||||||
if (navigateAfter) {
|
if (navigateAfter && workspace !== null) {
|
||||||
navigate(`/workspaces/${workspace?.id}/requests/${requestId}`);
|
routes.navigate('request', { workspaceId: workspace.id, requestId });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export function useDeleteRequest(id: string | null) {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<void, string>({
|
return useMutation<void, string>({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
console.log('DELETE REQUEST2', id, workspaceId);
|
||||||
if (id === null) return;
|
if (id === null) return;
|
||||||
await invoke('delete_request', { requestId: id });
|
await invoke('delete_request', { requestId: id });
|
||||||
},
|
},
|
||||||
|
|||||||
26
src-web/hooks/useDuplicateRequest.ts
Normal file
26
src-web/hooks/useDuplicateRequest.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
|
import { useRoutes } from './useRoutes';
|
||||||
|
|
||||||
|
export function useDuplicateRequest({
|
||||||
|
id,
|
||||||
|
navigateAfter,
|
||||||
|
}: {
|
||||||
|
id: string | null;
|
||||||
|
navigateAfter: boolean;
|
||||||
|
}) {
|
||||||
|
const workspaceId = useActiveWorkspaceId();
|
||||||
|
const routes = useRoutes();
|
||||||
|
return useMutation<string, string>({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (id === null) throw new Error("Can't duplicate a null request");
|
||||||
|
return invoke('duplicate_request', { id });
|
||||||
|
},
|
||||||
|
onSuccess: async (newId: string) => {
|
||||||
|
if (navigateAfter && workspaceId !== null) {
|
||||||
|
routes.navigate('request', { workspaceId, requestId: newId });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import { convertDates } from '../lib/models';
|
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
|
|
||||||
export function requestsQueryKey(workspaceId: string) {
|
export function requestsQueryKey(workspaceId: string) {
|
||||||
@@ -16,8 +15,7 @@ export function useRequests() {
|
|||||||
queryKey: requestsQueryKey(workspaceId ?? 'n/a'),
|
queryKey: requestsQueryKey(workspaceId ?? 'n/a'),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (workspaceId == null) return [];
|
if (workspaceId == null) return [];
|
||||||
const requests = (await invoke('requests', { workspaceId })) as HttpRequest[];
|
return (await invoke('requests', { workspaceId })) as HttpRequest[];
|
||||||
return requests.map(convertDates);
|
|
||||||
},
|
},
|
||||||
}).data ?? []
|
}).data ?? []
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import type { HttpResponse } from '../lib/models';
|
import type { HttpResponse } from '../lib/models';
|
||||||
import { convertDates } from '../lib/models';
|
|
||||||
|
|
||||||
export function responsesQueryKey(requestId: string) {
|
export function responsesQueryKey(requestId: string) {
|
||||||
return ['http_responses', { requestId }];
|
return ['http_responses', { requestId }];
|
||||||
@@ -14,10 +13,9 @@ export function useResponses(requestId: string | null) {
|
|||||||
initialData: [],
|
initialData: [],
|
||||||
queryKey: responsesQueryKey(requestId ?? 'n/a'),
|
queryKey: responsesQueryKey(requestId ?? 'n/a'),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const responses = (await invoke('responses', {
|
return (await invoke('responses', {
|
||||||
requestId,
|
requestId,
|
||||||
})) as HttpResponse[];
|
})) as HttpResponse[];
|
||||||
return responses.map(convertDates);
|
|
||||||
},
|
},
|
||||||
}).data ?? []
|
}).data ?? []
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export function useRoutes() {
|
|||||||
// outside caller perspective.
|
// outside caller perspective.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const resolvedPath = routePaths[path](...(params as any));
|
const resolvedPath = routePaths[path](...(params as any));
|
||||||
console.log('NAVIGATE TO', resolvedPath, 'WITH PARAMS', params, 'AND PATH', path);
|
|
||||||
navigate(resolvedPath);
|
navigate(resolvedPath);
|
||||||
},
|
},
|
||||||
paths: routePaths,
|
paths: routePaths,
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { responsesQueryKey } from './useResponses';
|
|
||||||
|
|
||||||
export function useSendRequest(id: string | null) {
|
export function useSendRequest(id: string | null) {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation<void, string>({
|
return useMutation<void, string>({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (id === null) return;
|
if (id === null) return;
|
||||||
await invoke('send_request', { requestId: id });
|
await invoke('send_request', { requestId: id });
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
|
||||||
if (id === null) return;
|
|
||||||
await queryClient.invalidateQueries(responsesQueryKey(id));
|
|
||||||
},
|
|
||||||
}).mutate;
|
}).mutate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ export function useUpdateRequest(id: string | null) {
|
|||||||
const updatedRequest = { ...request, ...patch };
|
const updatedRequest = { ...request, ...patch };
|
||||||
|
|
||||||
await invoke('update_request', {
|
await invoke('update_request', {
|
||||||
request: {
|
request: updatedRequest,
|
||||||
...updatedRequest,
|
|
||||||
createdAt: updatedRequest.createdAt.toISOString().replace('Z', ''),
|
|
||||||
updatedAt: updatedRequest.updatedAt.toISOString().replace('Z', ''),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import type { Workspace } from '../lib/models';
|
import type { Workspace } from '../lib/models';
|
||||||
import { convertDates } from '../lib/models';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
export function workspacesQueryKey() {
|
export function workspacesQueryKey() {
|
||||||
@@ -10,8 +9,7 @@ export function workspacesQueryKey() {
|
|||||||
export function useWorkspaces() {
|
export function useWorkspaces() {
|
||||||
return (
|
return (
|
||||||
useQuery(workspacesQueryKey(), async () => {
|
useQuery(workspacesQueryKey(), async () => {
|
||||||
const workspaces = (await invoke('workspaces')) as Workspace[];
|
return (await invoke('workspaces')) as Workspace[];
|
||||||
return workspaces.map(convertDates);
|
|
||||||
}).data ?? []
|
}).data ?? []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,20 +53,3 @@ export interface HttpResponse extends BaseModel {
|
|||||||
readonly url: string;
|
readonly url: string;
|
||||||
readonly headers: HttpHeader[];
|
readonly headers: HttpHeader[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertDates<T extends Pick<BaseModel, 'createdAt' | 'updatedAt'>>(m: T): T {
|
|
||||||
return {
|
|
||||||
...m,
|
|
||||||
createdAt: convertDate(m.createdAt),
|
|
||||||
updatedAt: convertDate(m.updatedAt),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertDate(d: string | Date): Date {
|
|
||||||
if (typeof d !== 'string') {
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
const date = new Date(d);
|
|
||||||
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
|
|
||||||
return new Date(date.getTime() - userTimezoneOffset);
|
|
||||||
}
|
|
||||||
|
|||||||
6
src-web/lib/sendEphemeralRequest.ts
Normal file
6
src-web/lib/sendEphemeralRequest.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import type { HttpRequest, HttpResponse } from './models';
|
||||||
|
|
||||||
|
export function sendEphemeralRequest(request: HttpRequest): Promise<HttpResponse> {
|
||||||
|
return invoke('send_ephemeral_request', { request });
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { convertDates } from './models';
|
|
||||||
import type { HttpRequest } from './models';
|
import type { HttpRequest } from './models';
|
||||||
|
|
||||||
export async function getRequest(id: string | null): Promise<HttpRequest | null> {
|
export async function getRequest(id: string | null): Promise<HttpRequest | null> {
|
||||||
@@ -8,5 +7,5 @@ export async function getRequest(id: string | null): Promise<HttpRequest | null>
|
|||||||
if (request == null) {
|
if (request == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return convertDates(request);
|
return request;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
const height = {
|
|
||||||
"xs": "1.5rem",
|
|
||||||
"sm": "2rem",
|
|
||||||
"md": "2.5rem"
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {import("tailwindcss").Config} */
|
/** @type {import("tailwindcss").Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ["class", "[data-appearance=\"dark\"]"],
|
darkMode: ["class", "[data-appearance=\"dark\"]"],
|
||||||
@@ -16,8 +10,17 @@ module.exports = {
|
|||||||
opacity: {
|
opacity: {
|
||||||
"disabled": "0.3"
|
"disabled": "0.3"
|
||||||
},
|
},
|
||||||
height,
|
height: {
|
||||||
lineHeight: height
|
"xs": "1.5rem",
|
||||||
|
"sm": "2.00rem",
|
||||||
|
"md": "2.5rem"
|
||||||
|
},
|
||||||
|
lineHeight: {
|
||||||
|
// HACK: Minus 2 to account for borders inside inputs
|
||||||
|
"xs": "calc(1.5rem - 2px)",
|
||||||
|
"sm": "calc(2.0rem - 2px)",
|
||||||
|
"md": "calc(2.5rem - 2px)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
"mono": ["JetBrains Mono", "Menlo", "monospace"],
|
"mono": ["JetBrains Mono", "Menlo", "monospace"],
|
||||||
|
|||||||
Reference in New Issue
Block a user