mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-07 05:15:20 +02:00
Focus request/folder after creation
This commit is contained in:
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
use log::{debug, error};
|
use log::error;
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime};
|
use tauri::{AppHandle, Emitter, Manager, Runtime};
|
||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
use yaak_common::window::WorkspaceWindowTrait;
|
use yaak_common::window::WorkspaceWindowTrait;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export type HttpUrlParameter = { enabled?: boolean, name: string, value: string,
|
|||||||
|
|
||||||
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
||||||
|
|
||||||
export type ModelChangeEvent = { "type": "upsert" } | { "type": "delete" };
|
export type ModelChangeEvent = { "type": "upsert", created: boolean, } | { "type": "delete" };
|
||||||
|
|
||||||
export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, };
|
export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, };
|
||||||
|
|
||||||
|
|||||||
@@ -12,31 +12,23 @@ export function initModelStore(store: JotaiStore) {
|
|||||||
_store = store;
|
_store = store;
|
||||||
|
|
||||||
getCurrentWebviewWindow()
|
getCurrentWebviewWindow()
|
||||||
.listen<ModelPayload>('upserted_model', ({ payload }) => {
|
.listen<ModelPayload>('model_write', ({ payload }) => {
|
||||||
if (shouldIgnoreModel(payload)) return;
|
if (shouldIgnoreModel(payload)) return;
|
||||||
|
|
||||||
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
||||||
return {
|
if (payload.change.type === 'upsert') {
|
||||||
...prev,
|
return {
|
||||||
[payload.model.model]: {
|
...prev,
|
||||||
...prev[payload.model.model],
|
[payload.model.model]: {
|
||||||
[payload.model.id]: payload.model,
|
...prev[payload.model.model],
|
||||||
},
|
[payload.model.id]: payload.model,
|
||||||
};
|
},
|
||||||
});
|
};
|
||||||
})
|
} else {
|
||||||
.catch(console.error);
|
const modelData = { ...prev[payload.model.model] };
|
||||||
|
delete modelData[payload.model.id];
|
||||||
getCurrentWebviewWindow()
|
return { ...prev, [payload.model.model]: modelData };
|
||||||
.listen<ModelPayload>('deleted_model', ({ payload }) => {
|
}
|
||||||
if (shouldIgnoreModel(payload)) return;
|
|
||||||
|
|
||||||
console.log('Delete model', payload);
|
|
||||||
|
|
||||||
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
|
||||||
const modelData = { ...prev[payload.model.model] };
|
|
||||||
delete modelData[payload.model.id];
|
|
||||||
return { ...prev, [payload.model.model]: modelData };
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ use crate::error::Error::ModelNotFound;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{AnyModel, UpsertModelInfo};
|
use crate::models::{AnyModel, UpsertModelInfo};
|
||||||
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
|
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
|
||||||
use log::error;
|
use log::{error, warn};
|
||||||
use rusqlite::OptionalExtension;
|
use rusqlite::OptionalExtension;
|
||||||
use sea_query::{
|
use sea_query::{
|
||||||
Asterisk, Expr, IntoColumnRef, IntoIden, IntoTableRef, OnConflict, Query, SimpleExpr,
|
Alias, Asterisk, Expr, Func, IntoColumnRef, IntoIden, IntoTableRef, OnConflict, Query,
|
||||||
SqliteQueryBuilder,
|
ReturningClause, SimpleExpr, SqliteQueryBuilder,
|
||||||
};
|
};
|
||||||
use sea_query_rusqlite::RusqliteBinder;
|
use sea_query_rusqlite::RusqliteBinder;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
@@ -152,21 +152,32 @@ impl<'a> DbContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let on_conflict = OnConflict::column(id_iden).update_columns(update_columns).to_owned();
|
let on_conflict = OnConflict::column(id_iden).update_columns(update_columns).to_owned();
|
||||||
|
|
||||||
let (sql, params) = Query::insert()
|
let (sql, params) = Query::insert()
|
||||||
.into_table(table)
|
.into_table(table)
|
||||||
.columns(column_vec)
|
.columns(column_vec)
|
||||||
.values_panic(value_vec)
|
.values_panic(value_vec)
|
||||||
.on_conflict(on_conflict)
|
.on_conflict(on_conflict)
|
||||||
.returning_all()
|
.returning(Query::returning().exprs(vec![
|
||||||
|
Expr::col(Asterisk),
|
||||||
|
Expr::expr(Func::cust("last_insert_rowid")),
|
||||||
|
Expr::col("rowid"),
|
||||||
|
]))
|
||||||
.build_rusqlite(SqliteQueryBuilder);
|
.build_rusqlite(SqliteQueryBuilder);
|
||||||
|
|
||||||
let mut stmt = self.conn.resolve().prepare(sql.as_str())?;
|
let mut stmt = self.conn.resolve().prepare(sql.as_str())?;
|
||||||
let m: M = stmt.query_row(&*params.as_params(), |row| M::from_row(row))?;
|
let (m, created): (M, bool) = stmt.query_row(&*params.as_params(), |row| {
|
||||||
|
M::from_row(row).and_then(|m| {
|
||||||
|
let rowid: i64 = row.get("rowid")?;
|
||||||
|
let last_rowid: i64 = row.get("last_insert_rowid()")?;
|
||||||
|
Ok((m, rowid == last_rowid))
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
let payload = ModelPayload {
|
let payload = ModelPayload {
|
||||||
model: m.clone().into(),
|
model: m.clone().into(),
|
||||||
update_source: source.clone(),
|
update_source: source.clone(),
|
||||||
change: ModelChangeEvent::Upsert,
|
change: ModelChangeEvent::Upsert { created },
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = self.events_tx.send(payload.clone()) {
|
if let Err(e) = self.events_tx.send(payload.clone()) {
|
||||||
|
|||||||
@@ -77,11 +77,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
for p in rx {
|
for p in rx {
|
||||||
let name = match p.change {
|
app_handle.emit("model_write", p).unwrap();
|
||||||
ModelChangeEvent::Upsert => "upserted_model",
|
|
||||||
ModelChangeEvent::Delete => "deleted_model",
|
|
||||||
};
|
|
||||||
app_handle.emit(name, p).unwrap();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use crate::models::{
|
|||||||
AnyModel, Environment, Folder, GrpcRequest, HttpRequest, UpsertModelInfo, WebsocketRequest,
|
AnyModel, Environment, Folder, GrpcRequest, HttpRequest, UpsertModelInfo, WebsocketRequest,
|
||||||
Workspace, WorkspaceIden,
|
Workspace, WorkspaceIden,
|
||||||
};
|
};
|
||||||
use yaak_common::window::WorkspaceWindowTrait;
|
|
||||||
use crate::query_manager::QueryManagerExt;
|
use crate::query_manager::QueryManagerExt;
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
@@ -12,6 +11,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use tauri::{AppHandle, Listener, Runtime, WebviewWindow};
|
use tauri::{AppHandle, Listener, Runtime, WebviewWindow};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
use yaak_common::window::WorkspaceWindowTrait;
|
||||||
|
|
||||||
pub fn generate_prefixed_id(prefix: &str) -> String {
|
pub fn generate_prefixed_id(prefix: &str) -> String {
|
||||||
format!("{prefix}_{}", generate_id())
|
format!("{prefix}_{}", generate_id())
|
||||||
@@ -45,7 +45,7 @@ pub struct ModelPayload {
|
|||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
pub enum ModelChangeEvent {
|
pub enum ModelChangeEvent {
|
||||||
Upsert,
|
Upsert { created: bool },
|
||||||
Delete,
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { showPrompt } from '../lib/prompt';
|
|||||||
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
|
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
|
||||||
|
|
||||||
export const createFolder = createFastMutation<
|
export const createFolder = createFastMutation<
|
||||||
void,
|
string | null,
|
||||||
void,
|
void,
|
||||||
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
|
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
|
||||||
>({
|
>({
|
||||||
@@ -34,13 +34,14 @@ export const createFolder = createFastMutation<
|
|||||||
confirmText: 'Create',
|
confirmText: 'Create',
|
||||||
placeholder: 'Name',
|
placeholder: 'Name',
|
||||||
});
|
});
|
||||||
if (name == null) return;
|
if (name == null) return null;
|
||||||
|
|
||||||
patch.name = name;
|
patch.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||||
await createWorkspaceModel({ model: 'folder', workspaceId, ...patch });
|
const id = await createWorkspaceModel({ model: 'folder', workspaceId, ...patch });
|
||||||
|
return id;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import type { Extension } from '@codemirror/state';
|
|||||||
import { Compartment } from '@codemirror/state';
|
import { Compartment } from '@codemirror/state';
|
||||||
import { debounce } from '@yaakapp-internal/lib';
|
import { debounce } from '@yaakapp-internal/lib';
|
||||||
import type {
|
import type {
|
||||||
|
AnyModel,
|
||||||
Folder,
|
Folder,
|
||||||
GrpcRequest,
|
GrpcRequest,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
|
ModelPayload,
|
||||||
WebsocketRequest,
|
WebsocketRequest,
|
||||||
Workspace,
|
Workspace,
|
||||||
} from '@yaakapp-internal/models';
|
} from '@yaakapp-internal/models';
|
||||||
@@ -34,6 +36,7 @@ import { getCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
|||||||
import { getGrpcRequestActions } from '../hooks/useGrpcRequestActions';
|
import { getGrpcRequestActions } from '../hooks/useGrpcRequestActions';
|
||||||
import { useHotKey } from '../hooks/useHotKey';
|
import { useHotKey } from '../hooks/useHotKey';
|
||||||
import { getHttpRequestActions } from '../hooks/useHttpRequestActions';
|
import { getHttpRequestActions } from '../hooks/useHttpRequestActions';
|
||||||
|
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||||
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
||||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||||
import { deepEqualAtom } from '../lib/atoms';
|
import { deepEqualAtom } from '../lib/atoms';
|
||||||
@@ -64,6 +67,15 @@ import type { TreeItemProps } from './core/tree/TreeItem';
|
|||||||
import { GitDropdown } from './GitDropdown';
|
import { GitDropdown } from './GitDropdown';
|
||||||
|
|
||||||
type SidebarModel = Workspace | Folder | HttpRequest | GrpcRequest | WebsocketRequest;
|
type SidebarModel = Workspace | Folder | HttpRequest | GrpcRequest | WebsocketRequest;
|
||||||
|
function isSidebarLeafModel(m: AnyModel): boolean {
|
||||||
|
const modelMap: Record<Exclude<SidebarModel['model'], 'workspace'>, null> = {
|
||||||
|
http_request: null,
|
||||||
|
grpc_request: null,
|
||||||
|
websocket_request: null,
|
||||||
|
folder: null,
|
||||||
|
};
|
||||||
|
return m.model in modelMap;
|
||||||
|
}
|
||||||
|
|
||||||
const OPACITY_SUBTLE = 'opacity-80';
|
const OPACITY_SUBTLE = 'opacity-80';
|
||||||
|
|
||||||
@@ -91,6 +103,13 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
if (!didFocus) filterRef.current?.focus();
|
if (!didFocus) filterRef.current?.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Focus any new sidebar models when created
|
||||||
|
useListenToTauriEvent<ModelPayload>('model_write', ({ payload }) => {
|
||||||
|
if (!isSidebarLeafModel(payload.model)) return;
|
||||||
|
if (!(payload.change.type === 'upsert' && payload.change.created)) return;
|
||||||
|
treeRef.current?.selectItem(payload.model.id, true);
|
||||||
|
});
|
||||||
|
|
||||||
useHotKey(
|
useHotKey(
|
||||||
'sidebar.filter',
|
'sidebar.filter',
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -11,16 +11,7 @@ import {
|
|||||||
import { type } from '@tauri-apps/plugin-os';
|
import { type } from '@tauri-apps/plugin-os';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react';
|
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react';
|
||||||
import {
|
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
||||||
forwardRef,
|
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useKey, useKeyPressEvent } from 'react-use';
|
import { useKey, useKeyPressEvent } from 'react-use';
|
||||||
import type { HotkeyAction, HotKeyOptions } from '../../../hooks/useHotKey';
|
import type { HotkeyAction, HotKeyOptions } from '../../../hooks/useHotKey';
|
||||||
import { useHotKey } from '../../../hooks/useHotKey';
|
import { useHotKey } from '../../../hooks/useHotKey';
|
||||||
@@ -72,7 +63,7 @@ export interface TreeHandle {
|
|||||||
treeId: string;
|
treeId: string;
|
||||||
focus: () => boolean;
|
focus: () => boolean;
|
||||||
hasFocus: () => boolean;
|
hasFocus: () => boolean;
|
||||||
selectItem: (id: string) => void;
|
selectItem: (id: string, focus?: boolean) => void;
|
||||||
renameItem: (id: string) => void;
|
renameItem: (id: string) => void;
|
||||||
showContextMenu: () => void;
|
showContextMenu: () => void;
|
||||||
}
|
}
|
||||||
@@ -181,11 +172,16 @@ function TreeInner<T extends { id: string }>(
|
|||||||
requestAnimationFrame(ensureTabbableItem);
|
requestAnimationFrame(ensureTabbableItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasFocus = useCallback(() => {
|
||||||
|
return treeRef.current?.contains(document.activeElement) ?? false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const setSelected = useCallback(
|
const setSelected = useCallback(
|
||||||
function setSelected(ids: string[], focus: boolean) {
|
(ids: string[], focus: boolean) => {
|
||||||
jotaiStore.set(selectedIdsFamily(treeId), ids);
|
jotaiStore.set(selectedIdsFamily(treeId), ids);
|
||||||
// TODO: Figure out a better way than timeout
|
// TODO: Figure out a better way than timeout
|
||||||
if (focus) setTimeout(tryFocus, 50);
|
if (!focus) return;
|
||||||
|
setTimeout(tryFocus, 50);
|
||||||
},
|
},
|
||||||
[treeId, tryFocus],
|
[treeId, tryFocus],
|
||||||
);
|
);
|
||||||
@@ -194,15 +190,15 @@ function TreeInner<T extends { id: string }>(
|
|||||||
() => ({
|
() => ({
|
||||||
treeId,
|
treeId,
|
||||||
focus: tryFocus,
|
focus: tryFocus,
|
||||||
hasFocus: () => treeRef.current?.contains(document.activeElement) ?? false,
|
hasFocus: hasFocus,
|
||||||
renameItem: (id) => treeItemRefs.current[id]?.rename(),
|
renameItem: (id) => treeItemRefs.current[id]?.rename(),
|
||||||
selectItem: (id) => {
|
selectItem: (id, focus) => {
|
||||||
if (jotaiStore.get(selectedIdsFamily(treeId)).includes(id)) {
|
if (jotaiStore.get(selectedIdsFamily(treeId)).includes(id)) {
|
||||||
// Already selected
|
// Already selected
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelected([id], false);
|
|
||||||
jotaiStore.set(focusIdsFamily(treeId), { anchorId: id, lastId: id });
|
jotaiStore.set(focusIdsFamily(treeId), { anchorId: id, lastId: id });
|
||||||
|
setSelected([id], focus === true);
|
||||||
},
|
},
|
||||||
showContextMenu: async () => {
|
showContextMenu: async () => {
|
||||||
if (getContextMenu == null) return;
|
if (getContextMenu == null) return;
|
||||||
@@ -214,7 +210,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
setShowContextMenu({ items: menuItems, x: rect.x, y: rect.y });
|
setShowContextMenu({ items: menuItems, x: rect.x, y: rect.y });
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[getContextMenu, selectableItems, setSelected, treeId, tryFocus],
|
[getContextMenu, hasFocus, selectableItems, setSelected, treeId, tryFocus],
|
||||||
);
|
);
|
||||||
|
|
||||||
useImperativeHandle(ref, (): TreeHandle => treeHandle, [treeHandle]);
|
useImperativeHandle(ref, (): TreeHandle => treeHandle, [treeHandle]);
|
||||||
@@ -248,7 +244,9 @@ function TreeInner<T extends { id: string }>(
|
|||||||
|
|
||||||
if (shiftKey) {
|
if (shiftKey) {
|
||||||
const validSelectableItems = getValidSelectableItems(treeId, selectableItems);
|
const validSelectableItems = getValidSelectableItems(treeId, selectableItems);
|
||||||
const anchorIndex = validSelectableItems.findIndex((i) => i.node.item.id === anchorSelectedId);
|
const anchorIndex = validSelectableItems.findIndex(
|
||||||
|
(i) => i.node.item.id === anchorSelectedId,
|
||||||
|
);
|
||||||
const currIndex = validSelectableItems.findIndex((v) => v.node.item.id === item.id);
|
const currIndex = validSelectableItems.findIndex((v) => v.node.item.id === item.id);
|
||||||
|
|
||||||
// Nothing was selected yet, so just select this item
|
// Nothing was selected yet, so just select this item
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ function setFontSizeOnDocument(fontSize: number) {
|
|||||||
document.documentElement.style.fontSize = `${fontSize}px`;
|
document.documentElement.style.fontSize = `${fontSize}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen<ModelPayload>('upserted_model', async (event) => {
|
listen<ModelPayload>('model_write', async (event) => {
|
||||||
|
if (event.payload.change.type !== 'upsert') return;
|
||||||
if (event.payload.model.model !== 'settings') return;
|
if (event.payload.model.model !== 'settings') return;
|
||||||
setFontSizeOnDocument(event.payload.model.interfaceFontSize);
|
setFontSizeOnDocument(event.payload.model.interfaceFontSize);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,8 @@ function setFonts(settings: Settings) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
listen<ModelPayload>('upserted_model', async (event) => {
|
listen<ModelPayload>('model_write', async (event) => {
|
||||||
|
if (event.payload.change.type !== 'upsert') return;
|
||||||
if (event.payload.model.model !== 'settings') return;
|
if (event.payload.model.model !== 'settings') return;
|
||||||
setFonts(event.payload.model);
|
setFonts(event.payload.model);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
|
|||||||
@@ -36,46 +36,68 @@ export function getCreateDropdownItems({
|
|||||||
folderId: folderIdOption,
|
folderId: folderIdOption,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
activeRequest,
|
activeRequest,
|
||||||
|
onCreate,
|
||||||
}: {
|
}: {
|
||||||
hideFolder?: boolean;
|
hideFolder?: boolean;
|
||||||
hideIcons?: boolean;
|
hideIcons?: boolean;
|
||||||
folderId?: string | null | 'active-folder';
|
folderId?: string | null | 'active-folder';
|
||||||
workspaceId: string | null;
|
workspaceId: string | null;
|
||||||
activeRequest: HttpRequest | GrpcRequest | WebsocketRequest | null;
|
activeRequest: HttpRequest | GrpcRequest | WebsocketRequest | null;
|
||||||
|
onCreate?: (
|
||||||
|
model: 'http_request' | 'grpc_request' | 'websocket_request' | 'folder',
|
||||||
|
id: string,
|
||||||
|
) => void;
|
||||||
}): DropdownItem[] {
|
}): DropdownItem[] {
|
||||||
const folderId =
|
const folderId =
|
||||||
(folderIdOption === 'active-folder' ? activeRequest?.folderId : folderIdOption) ?? null;
|
(folderIdOption === 'active-folder' ? activeRequest?.folderId : folderIdOption) ?? null;
|
||||||
if (workspaceId == null) return [];
|
|
||||||
|
if (workspaceId == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'HTTP',
|
label: 'HTTP',
|
||||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||||
onSelect: () => createRequestAndNavigate({ model: 'http_request', workspaceId, folderId }),
|
onSelect: async () => {
|
||||||
|
const id = await createRequestAndNavigate({ model: 'http_request', workspaceId, folderId });
|
||||||
|
onCreate?.('http_request', id);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'GraphQL',
|
label: 'GraphQL',
|
||||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||||
onSelect: () =>
|
onSelect: async () => {
|
||||||
createRequestAndNavigate({
|
const id = await createRequestAndNavigate({
|
||||||
model: 'http_request',
|
model: 'http_request',
|
||||||
workspaceId,
|
workspaceId,
|
||||||
folderId,
|
folderId,
|
||||||
bodyType: BODY_TYPE_GRAPHQL,
|
bodyType: BODY_TYPE_GRAPHQL,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: [{ name: 'Content-Type', value: 'application/json', id: generateId() }],
|
headers: [{ name: 'Content-Type', value: 'application/json', id: generateId() }],
|
||||||
}),
|
});
|
||||||
|
onCreate?.('http_request', id);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'gRPC',
|
label: 'gRPC',
|
||||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||||
onSelect: () => createRequestAndNavigate({ model: 'grpc_request', workspaceId, folderId }),
|
onSelect: async () => {
|
||||||
|
const id = await createRequestAndNavigate({ model: 'grpc_request', workspaceId, folderId });
|
||||||
|
onCreate?.('grpc_request', id);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'WebSocket',
|
label: 'WebSocket',
|
||||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||||
onSelect: () =>
|
onSelect: async () => {
|
||||||
createRequestAndNavigate({ model: 'websocket_request', workspaceId, folderId }),
|
const id = await createRequestAndNavigate({
|
||||||
|
model: 'websocket_request',
|
||||||
|
workspaceId,
|
||||||
|
folderId,
|
||||||
|
});
|
||||||
|
onCreate?.('websocket_request', id);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...((hideFolder
|
...((hideFolder
|
||||||
? []
|
? []
|
||||||
@@ -84,7 +106,12 @@ export function getCreateDropdownItems({
|
|||||||
{
|
{
|
||||||
label: 'Folder',
|
label: 'Folder',
|
||||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||||
onSelect: () => createFolder.mutate({ folderId }),
|
onSelect: async () => {
|
||||||
|
const id = await createFolder.mutateAsync({ folderId });
|
||||||
|
if (id != null) {
|
||||||
|
onCreate?.('folder', id);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]) as DropdownItem[]),
|
]) as DropdownItem[]),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import type { EventCallback, EventName } from '@tauri-apps/api/event';
|
import type { EventCallback, EventName } from '@tauri-apps/api/event';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
/**
|
|
||||||
* React hook to listen to a Tauri event.
|
|
||||||
*/
|
|
||||||
export function useListenToTauriEvent<T>(event: EventName, fn: EventCallback<T>) {
|
export function useListenToTauriEvent<T>(event: EventName, fn: EventCallback<T>) {
|
||||||
useEffect(() => listenToTauriEvent(event, fn), [event, fn]);
|
const handlerRef = useRef(fn);
|
||||||
|
useEffect(() => {
|
||||||
|
handlerRef.current = fn;
|
||||||
|
}, [fn]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return listenToTauriEvent<T>(event, (p) => handlerRef.current(p));
|
||||||
|
}, [event]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listenToTauriEvent<T>(event: EventName, fn: EventCallback<T>) {
|
export function listenToTauriEvent<T>(event: EventName, fn: EventCallback<T>) {
|
||||||
const unlisten = listen<T>(
|
const unsubPromise = listen<T>(
|
||||||
event,
|
event,
|
||||||
fn,
|
fn,
|
||||||
// Listen to `emit_all()` events or events specific to the current window
|
// Listen to `emit_all()` events or events specific to the current window
|
||||||
@@ -19,6 +23,6 @@ export function listenToTauriEvent<T>(event: EventName, fn: EventCallback<T>) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlisten.then((fn) => fn());
|
unsubPromise.then((unsub) => unsub()).catch(console.error);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { jotaiStore } from '../lib/jotai';
|
|||||||
const requestUpdateKeyAtom = atom<Record<string, string>>({});
|
const requestUpdateKeyAtom = atom<Record<string, string>>({});
|
||||||
|
|
||||||
getCurrentWebviewWindow()
|
getCurrentWebviewWindow()
|
||||||
.listen<ModelPayload>('upserted_model', ({ payload }) => {
|
.listen<ModelPayload>('model_write', ({ payload }) => {
|
||||||
|
if (payload.change.type !== 'upsert') return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(payload.model.model === 'http_request' ||
|
(payload.model.model === 'http_request' ||
|
||||||
payload.model.model === 'grpc_request' ||
|
payload.model.model === 'grpc_request' ||
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ const debouncedSync = debounce(async () => {
|
|||||||
* simply add long-lived subscribers for the lifetime of the app.
|
* simply add long-lived subscribers for the lifetime of the app.
|
||||||
*/
|
*/
|
||||||
function initModelListeners() {
|
function initModelListeners() {
|
||||||
listenToTauriEvent<ModelPayload>('upserted_model', (p) => {
|
listenToTauriEvent<ModelPayload>('model_write', (p) => {
|
||||||
if (isModelRelevant(p.payload.model)) debouncedSync();
|
|
||||||
});
|
|
||||||
listenToTauriEvent<ModelPayload>('deleted_model', (p) => {
|
|
||||||
if (isModelRelevant(p.payload.model)) debouncedSync();
|
if (isModelRelevant(p.payload.model)) debouncedSync();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export async function createRequestAndNavigate<
|
|||||||
|
|
||||||
if (patch.sortPriority === undefined) {
|
if (patch.sortPriority === undefined) {
|
||||||
if (activeRequest != null) {
|
if (activeRequest != null) {
|
||||||
// Place above currently active request
|
// Place below the currently active request
|
||||||
patch.sortPriority = activeRequest.sortPriority - 0.0001;
|
patch.sortPriority = activeRequest.sortPriority;
|
||||||
} else {
|
} else {
|
||||||
// Place at the very top
|
// Place at the very top
|
||||||
patch.sortPriority = -Date.now();
|
patch.sortPriority = -Date.now();
|
||||||
@@ -27,4 +27,5 @@ export async function createRequestAndNavigate<
|
|||||||
params: { workspaceId: patch.workspaceId },
|
params: { workspaceId: patch.workspaceId },
|
||||||
search: (prev) => ({ ...prev, request_id: newId }),
|
search: (prev) => ({ ...prev, request_id: newId }),
|
||||||
});
|
});
|
||||||
|
return newId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function duplicateRequestOrFolderAndNavigate(
|
|||||||
|
|
||||||
const newId = await duplicateModel(model);
|
const newId = await duplicateModel(model);
|
||||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||||
if (workspaceId == null) return;
|
if (workspaceId == null || model.model === 'folder') return;
|
||||||
|
|
||||||
navigateToRequestOrFolderOrWorkspace(newId, model.model);
|
navigateToRequestOrFolderOrWorkspace(newId, model.model);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function setWorkspaceSearchParams(
|
|||||||
(router as any).navigate({
|
(router as any).navigate({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
search: (prev: any) => {
|
search: (prev: any) => {
|
||||||
console.log('Navigating to', { prev, search });
|
// console.log('Navigating to', { prev, search });
|
||||||
return { ...prev, ...search };
|
return { ...prev, ...search };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-1
@@ -26,7 +26,9 @@ configureTheme().then(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Listen for settings changes, the re-compute theme
|
// Listen for settings changes, the re-compute theme
|
||||||
listen<ModelPayload>('upserted_model', async (event) => {
|
listen<ModelPayload>('model_write', async (event) => {
|
||||||
|
if (event.payload.change.type !== 'upsert') return;
|
||||||
|
|
||||||
const model = event.payload.model.model;
|
const model = event.payload.model.model;
|
||||||
if (model !== 'settings' && model !== 'plugin') return;
|
if (model !== 'settings' && model !== 'plugin') return;
|
||||||
await configureTheme();
|
await configureTheme();
|
||||||
|
|||||||
Reference in New Issue
Block a user