mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-05-05 23:43:45 +02:00
New sidebar and folder view (#263)
This commit is contained in:
9
src-web/hooks/useActiveFolder.ts
Normal file
9
src-web/hooks/useActiveFolder.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { foldersAtom } from '@yaakapp-internal/models';
|
||||
import { atom } from 'jotai';
|
||||
import { activeFolderIdAtom } from './useActiveFolderId';
|
||||
|
||||
export const activeFolderAtom = atom((get) => {
|
||||
const activeFolderId = get(activeFolderIdAtom);
|
||||
const folders = get(foldersAtom);
|
||||
return folders.find((r) => r.id === activeFolderId) ?? null;
|
||||
});
|
||||
11
src-web/hooks/useActiveFolderId.ts
Normal file
11
src-web/hooks/useActiveFolderId.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useSearch } from '@tanstack/react-router';
|
||||
import { atom } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
|
||||
export const activeFolderIdAtom = atom<string | null>(null);
|
||||
|
||||
export function useSubscribeActiveFolderId() {
|
||||
const { folder_id } = useSearch({ strict: false });
|
||||
useEffect(() => jotaiStore.set(activeFolderIdAtom, folder_id ?? null), [folder_id]);
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
import { useSearch } from '@tanstack/react-router';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import { atom } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
|
||||
export const activeRequestIdAtom = atom<string | null>(null);
|
||||
|
||||
export function useActiveRequestId(): string | null {
|
||||
return useAtomValue(activeRequestIdAtom);
|
||||
}
|
||||
|
||||
export function useSubscribeActiveRequestId() {
|
||||
const { request_id } = useSearch({ strict: false });
|
||||
useEffect(() => jotaiStore.set(activeRequestIdAtom, request_id ?? null), [request_id]);
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import type { Folder } from '@yaakapp-internal/models';
|
||||
import { patchModel } from '@yaakapp-internal/models';
|
||||
import { useMemo } from 'react';
|
||||
import { openFolderSettings } from '../commands/openFolderSettings';
|
||||
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
|
||||
import { Icon } from '../components/core/Icon';
|
||||
import { IconTooltip } from '../components/core/IconTooltip';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { HStack } from '../components/core/Stacks';
|
||||
import type { TabItem } from '../components/core/Tabs/Tabs';
|
||||
import { capitalize } from '../lib/capitalize';
|
||||
import { showConfirm } from '../lib/confirm';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { useHttpAuthenticationSummaries } from './useHttpAuthentication';
|
||||
import type { AuthenticatedModel} from './useInheritedAuthentication';
|
||||
import type { AuthenticatedModel } from './useInheritedAuthentication';
|
||||
import { useInheritedAuthentication } from './useInheritedAuthentication';
|
||||
import { useModelAncestors } from './useModelAncestors';
|
||||
|
||||
export function useAuthTab<T extends string>(tabValue: T, model: AuthenticatedModel | null) {
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
const inheritedAuth = useInheritedAuthentication(model);
|
||||
const ancestors = useModelAncestors(model);
|
||||
const parentModel = ancestors[0] ?? null;
|
||||
|
||||
return useMemo<TabItem[]>(() => {
|
||||
if (model == null) return [];
|
||||
@@ -47,6 +57,49 @@ export function useAuthTab<T extends string>(tabValue: T, model: AuthenticatedMo
|
||||
},
|
||||
{ label: 'No Auth', shortLabel: 'No Auth', value: 'none' },
|
||||
],
|
||||
itemsAfter:
|
||||
parentModel &&
|
||||
model.authenticationType &&
|
||||
model.authenticationType !== 'none' &&
|
||||
(parentModel.authenticationType == null || parentModel.authenticationType === 'none')
|
||||
? [
|
||||
{ type: 'separator', label: 'Actions' },
|
||||
{
|
||||
label: `Promote to ${capitalize(parentModel.model)}`,
|
||||
leftSlot: (
|
||||
<Icon
|
||||
icon={parentModel.model === 'workspace' ? 'corner_right_up' : 'folder_up'}
|
||||
/>
|
||||
),
|
||||
onSelect: async () => {
|
||||
const confirmed = await showConfirm({
|
||||
id: 'promote-auth-confirm',
|
||||
title: 'Promote Authentication',
|
||||
confirmText: 'Promote',
|
||||
description: (
|
||||
<>
|
||||
Move authentication config to{' '}
|
||||
<InlineCode>{resolvedModelName(parentModel)}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (confirmed) {
|
||||
await patchModel(model, { authentication: {}, authenticationType: null });
|
||||
await patchModel(parentModel, {
|
||||
authentication: model.authentication,
|
||||
authenticationType: model.authenticationType,
|
||||
});
|
||||
|
||||
if (parentModel.model === 'folder') {
|
||||
openFolderSettings(parentModel.id, 'auth');
|
||||
} else {
|
||||
openWorkspaceSettings('auth');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
onChange: async (authenticationType) => {
|
||||
let authentication: Folder['authentication'] = model.authentication;
|
||||
if (model.authenticationType !== authenticationType) {
|
||||
@@ -60,5 +113,5 @@ export function useAuthTab<T extends string>(tabValue: T, model: AuthenticatedMo
|
||||
};
|
||||
|
||||
return [tab];
|
||||
}, [authentication, inheritedAuth, model, tabValue]);
|
||||
}, [authentication, inheritedAuth, model, parentModel, tabValue]);
|
||||
}
|
||||
|
||||
@@ -20,25 +20,7 @@ export function useGrpcRequestActions() {
|
||||
const actionsResult = useQuery<CallableGrpcRequestAction[]>({
|
||||
queryKey: ['grpc_request_actions', pluginsKey],
|
||||
queryFn: async () => {
|
||||
const responses = await invokeCmd<GetGrpcRequestActionsResponse[]>(
|
||||
'cmd_grpc_request_actions',
|
||||
);
|
||||
|
||||
return responses.flatMap((r) =>
|
||||
r.actions.map((a, i) => ({
|
||||
label: a.label,
|
||||
icon: a.icon,
|
||||
call: async (grpcRequest: GrpcRequest) => {
|
||||
const protoFiles = await getGrpcProtoFiles(grpcRequest.id);
|
||||
const payload: CallGrpcRequestActionRequest = {
|
||||
index: i,
|
||||
pluginRefId: r.pluginRefId,
|
||||
args: { grpcRequest, protoFiles },
|
||||
};
|
||||
await invokeCmd('cmd_call_grpc_request_action', { req: payload });
|
||||
},
|
||||
})),
|
||||
);
|
||||
return getGrpcRequestActions();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,3 +31,23 @@ export function useGrpcRequestActions() {
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export async function getGrpcRequestActions() {
|
||||
const responses = await invokeCmd<GetGrpcRequestActionsResponse[]>('cmd_grpc_request_actions');
|
||||
|
||||
return responses.flatMap((r) =>
|
||||
r.actions.map((a, i) => ({
|
||||
label: a.label,
|
||||
icon: a.icon,
|
||||
call: async (grpcRequest: GrpcRequest) => {
|
||||
const protoFiles = await getGrpcProtoFiles(grpcRequest.id);
|
||||
const payload: CallGrpcRequestActionRequest = {
|
||||
index: i,
|
||||
pluginRefId: r.pluginRefId,
|
||||
args: { grpcRequest, protoFiles },
|
||||
};
|
||||
await invokeCmd('cmd_call_grpc_request_action', { req: payload });
|
||||
},
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { debounce } from '@yaakapp-internal/lib';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { atom } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { capitalize } from '../lib/capitalize';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
|
||||
const HOLD_KEYS = ['Shift', 'Control', 'Command', 'Alt', 'Meta'];
|
||||
|
||||
@@ -11,11 +13,10 @@ export type HotkeyAction =
|
||||
| 'app.zoom_reset'
|
||||
| 'command_palette.toggle'
|
||||
| 'environmentEditor.toggle'
|
||||
| 'grpc_request.send'
|
||||
| 'hotkeys.showHelp'
|
||||
| 'http_request.create'
|
||||
| 'http_request.duplicate'
|
||||
| 'http_request.send'
|
||||
| 'model.create'
|
||||
| 'model.duplicate'
|
||||
| 'request.send'
|
||||
| 'request_switcher.next'
|
||||
| 'request_switcher.prev'
|
||||
| 'request_switcher.toggle'
|
||||
@@ -31,11 +32,10 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'app.zoom_reset': ['CmdCtrl+0'],
|
||||
'command_palette.toggle': ['CmdCtrl+k'],
|
||||
'environmentEditor.toggle': ['CmdCtrl+Shift+E', 'CmdCtrl+Shift+e'],
|
||||
'grpc_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||
'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||
'hotkeys.showHelp': ['CmdCtrl+Shift+/', 'CmdCtrl+Shift+?'], // when shift is pressed, it might be a question mark
|
||||
'http_request.create': ['CmdCtrl+n'],
|
||||
'http_request.duplicate': ['CmdCtrl+d'],
|
||||
'http_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||
'model.create': ['CmdCtrl+n'],
|
||||
'model.duplicate': ['CmdCtrl+d'],
|
||||
'request_switcher.next': ['Control+Shift+Tab'],
|
||||
'request_switcher.prev': ['Control+Tab'],
|
||||
'request_switcher.toggle': ['CmdCtrl+p'],
|
||||
@@ -52,11 +52,10 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
'app.zoom_reset': 'Zoom to Actual Size',
|
||||
'command_palette.toggle': 'Toggle Command Palette',
|
||||
'environmentEditor.toggle': 'Edit Environments',
|
||||
'grpc_request.send': 'Send Message',
|
||||
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
|
||||
'http_request.create': 'New Request',
|
||||
'http_request.duplicate': 'Duplicate Request',
|
||||
'http_request.send': 'Send Request',
|
||||
'model.create': 'New Request',
|
||||
'model.duplicate': 'Duplicate Request',
|
||||
'request.send': 'Send',
|
||||
'request_switcher.next': 'Go To Previous Request',
|
||||
'request_switcher.prev': 'Go To Next Request',
|
||||
'request_switcher.toggle': 'Toggle Request Switcher',
|
||||
@@ -71,108 +70,139 @@ const layoutInsensitiveKeys = ['Equal', 'Minus', 'BracketLeft', 'BracketRight',
|
||||
|
||||
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||
|
||||
interface Options {
|
||||
enable?: boolean;
|
||||
export type HotKeyOptions = {
|
||||
enable?: boolean | (() => boolean);
|
||||
priority?: number;
|
||||
};
|
||||
|
||||
interface Callback {
|
||||
action: HotkeyAction;
|
||||
callback: (e: KeyboardEvent) => void;
|
||||
options: HotKeyOptions;
|
||||
}
|
||||
|
||||
const callbacksAtom = atom<Callback[]>([]);
|
||||
const currentKeysAtom = atom<Set<string>>(new Set([]));
|
||||
export const sortedCallbacksAtom = atom((get) =>
|
||||
[...get(callbacksAtom)].sort((a, b) => (b.options.priority ?? 0) - (a.options.priority ?? 0)),
|
||||
);
|
||||
|
||||
const clearCurrentKeysDebounced = debounce(() => {
|
||||
jotaiStore.set(currentKeysAtom, new Set([]));
|
||||
}, 5000);
|
||||
|
||||
export function useHotKey(
|
||||
action: HotkeyAction | null,
|
||||
callback: (e: KeyboardEvent) => void,
|
||||
options: Options = {},
|
||||
options: HotKeyOptions = {},
|
||||
) {
|
||||
const currentKeys = useRef<Set<string>>(new Set());
|
||||
const callbackRef = useRef(callback);
|
||||
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
// Sometimes the keyup event doesn't fire (eg, cmd+Tab), so we clear the keys after a timeout
|
||||
const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 5000);
|
||||
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (options.enable === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't add key if not holding modifier
|
||||
const isValidKeymapKey =
|
||||
e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace' || e.key === 'Delete';
|
||||
if (!isValidKeymapKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't add hold keys
|
||||
if (HOLD_KEYS.includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyToAdd = layoutInsensitiveKeys.includes(e.code) ? e.code : e.key;
|
||||
currentKeys.current.add(keyToAdd);
|
||||
|
||||
const currentKeysWithModifiers = new Set(currentKeys.current);
|
||||
if (e.altKey) currentKeysWithModifiers.add('Alt');
|
||||
if (e.ctrlKey) currentKeysWithModifiers.add('Control');
|
||||
if (e.metaKey) currentKeysWithModifiers.add('Meta');
|
||||
if (e.shiftKey) currentKeysWithModifiers.add('Shift');
|
||||
|
||||
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
||||
if (
|
||||
(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) &&
|
||||
currentKeysWithModifiers.size === 1 &&
|
||||
currentKeysWithModifiers.has('Backspace')
|
||||
) {
|
||||
// Don't support Backspace-only modifiers within input fields. This is fairly brittle, so maybe there's a
|
||||
// better way to do stuff like this in the future.
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const hkKey of hkKeys) {
|
||||
if (hkAction !== action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keys = hkKey.split('+').map(resolveHotkeyKey);
|
||||
if (
|
||||
keys.length === currentKeysWithModifiers.size &&
|
||||
keys.every((key) => currentKeysWithModifiers.has(key))
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
callbackRef.current(e);
|
||||
currentKeys.current.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearCurrentKeys();
|
||||
};
|
||||
|
||||
const up = (e: KeyboardEvent) => {
|
||||
if (options.enable === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyToRemove = layoutInsensitiveKeys.includes(e.code) ? e.code : e.key;
|
||||
currentKeys.current.delete(keyToRemove);
|
||||
|
||||
// Clear all keys if no longer holding modifier
|
||||
// HACK: This is to get around the case of DOWN SHIFT -> DOWN : -> UP SHIFT -> UP ;
|
||||
// As you see, the ":" is not removed because it turned into ";" when shift was released
|
||||
const isHoldingModifier = e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
|
||||
if (!isHoldingModifier) {
|
||||
currentKeys.current.clear();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keyup', up, { capture: true });
|
||||
document.addEventListener('keydown', down, { capture: true });
|
||||
if (action == null) return;
|
||||
jotaiStore.set(callbacksAtom, (prev) => {
|
||||
const without = prev.filter((cb) => {
|
||||
const isTheSame = cb.action === action && cb.options.priority === options.priority;
|
||||
return !isTheSame;
|
||||
});
|
||||
const newCb: Callback = { action, callback, options };
|
||||
return [...without, newCb];
|
||||
});
|
||||
return () => {
|
||||
document.removeEventListener('keydown', down, { capture: true });
|
||||
document.removeEventListener('keyup', up, { capture: true });
|
||||
jotaiStore.set(callbacksAtom, (prev) => prev.filter((cb) => cb.action !== action));
|
||||
};
|
||||
}, [action, options.enable]);
|
||||
}, [action, callback, options]);
|
||||
}
|
||||
|
||||
export function useSubscribeHotKeys() {
|
||||
useEffect(() => {
|
||||
document.addEventListener('keyup', handleKeyUp, { capture: true });
|
||||
document.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||
document.removeEventListener('keyup', handleKeyUp, { capture: true });
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
function handleKeyUp(e: KeyboardEvent) {
|
||||
const keyToRemove = layoutInsensitiveKeys.includes(e.code) ? e.code : e.key;
|
||||
const currentKeys = new Set(jotaiStore.get(currentKeysAtom));
|
||||
currentKeys.delete(keyToRemove);
|
||||
|
||||
// Clear all keys if no longer holding modifier
|
||||
// HACK: This is to get around the case of DOWN SHIFT -> DOWN : -> UP SHIFT -> UP ;
|
||||
// As you see, the ":" is not removed because it turned into ";" when shift was released
|
||||
const isHoldingModifier = e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
|
||||
if (!isHoldingModifier) {
|
||||
currentKeys.clear();
|
||||
}
|
||||
|
||||
jotaiStore.set(currentKeysAtom, currentKeys);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Don't add key if not holding modifier
|
||||
const isValidKeymapKey =
|
||||
e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace' || e.key === 'Delete';
|
||||
if (!isValidKeymapKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't add hold keys
|
||||
if (HOLD_KEYS.includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyToAdd = layoutInsensitiveKeys.includes(e.code) ? e.code : e.key;
|
||||
const currentKeys = new Set(jotaiStore.get(currentKeysAtom));
|
||||
currentKeys.add(keyToAdd);
|
||||
|
||||
const currentKeysWithModifiers = new Set(currentKeys);
|
||||
if (e.altKey) currentKeysWithModifiers.add('Alt');
|
||||
if (e.ctrlKey) currentKeysWithModifiers.add('Control');
|
||||
if (e.metaKey) currentKeysWithModifiers.add('Meta');
|
||||
if (e.shiftKey) currentKeysWithModifiers.add('Shift');
|
||||
|
||||
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
||||
if (
|
||||
(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) &&
|
||||
currentKeysWithModifiers.size === 1 &&
|
||||
currentKeysWithModifiers.has('Backspace')
|
||||
) {
|
||||
// Don't support Backspace-only modifiers within input fields. This is fairly brittle, so maybe there's a
|
||||
// better way to do stuff like this in the future.
|
||||
continue;
|
||||
}
|
||||
|
||||
const executed: string[] = [];
|
||||
for (const { action, callback, options } of jotaiStore.get(sortedCallbacksAtom)) {
|
||||
const enable = typeof options.enable === 'function' ? options.enable() : options.enable;
|
||||
if (enable === false) {
|
||||
continue;
|
||||
}
|
||||
if (hkAction !== action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const hkKey of hkKeys) {
|
||||
const keys = hkKey.split('+').map(resolveHotkeyKey);
|
||||
if (
|
||||
keys.length === currentKeysWithModifiers.size &&
|
||||
keys.every((key) => currentKeysWithModifiers.has(key))
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
callback(e);
|
||||
executed.push(`${action} ${options.priority ?? 0}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (executed.length > 0) {
|
||||
console.log('Executed hotkey', executed.join(', '));
|
||||
jotaiStore.set(currentKeysAtom, new Set([]));
|
||||
}
|
||||
}
|
||||
|
||||
clearCurrentKeysDebounced();
|
||||
}
|
||||
|
||||
export function useHotKeyLabel(action: HotkeyAction): string {
|
||||
|
||||
@@ -18,26 +18,7 @@ export function useHttpRequestActions() {
|
||||
|
||||
const actionsResult = useQuery<CallableHttpRequestAction[]>({
|
||||
queryKey: ['http_request_actions', pluginsKey],
|
||||
queryFn: async () => {
|
||||
const responses = await invokeCmd<GetHttpRequestActionsResponse[]>(
|
||||
'cmd_http_request_actions',
|
||||
);
|
||||
|
||||
return responses.flatMap((r) =>
|
||||
r.actions.map((a, i) => ({
|
||||
label: a.label,
|
||||
icon: a.icon,
|
||||
call: async (httpRequest: HttpRequest) => {
|
||||
const payload: CallHttpRequestActionRequest = {
|
||||
index: i,
|
||||
pluginRefId: r.pluginRefId,
|
||||
args: { httpRequest },
|
||||
};
|
||||
await invokeCmd('cmd_call_http_request_action', { req: payload });
|
||||
},
|
||||
})),
|
||||
);
|
||||
},
|
||||
queryFn: () => getHttpRequestActions(),
|
||||
});
|
||||
|
||||
const actions = useMemo(() => {
|
||||
@@ -47,3 +28,23 @@ export function useHttpRequestActions() {
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export async function getHttpRequestActions() {
|
||||
const responses = await invokeCmd<GetHttpRequestActionsResponse[]>('cmd_http_request_actions');
|
||||
const actions = responses.flatMap((r) =>
|
||||
r.actions.map((a, i) => ({
|
||||
label: a.label,
|
||||
icon: a.icon,
|
||||
call: async (httpRequest: HttpRequest) => {
|
||||
const payload: CallHttpRequestActionRequest = {
|
||||
index: i,
|
||||
pluginRefId: r.pluginRefId,
|
||||
args: { httpRequest },
|
||||
};
|
||||
await invokeCmd('cmd_call_http_request_action', { req: payload });
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
41
src-web/hooks/useModelAncestors.ts
Normal file
41
src-web/hooks/useModelAncestors.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { AnyModel, Folder, Workspace } from '@yaakapp-internal/models';
|
||||
import { foldersAtom, workspacesAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type ModelAncestor = Folder | Workspace;
|
||||
|
||||
export function useModelAncestors(m: AnyModel | null) {
|
||||
const folders = useAtomValue(foldersAtom);
|
||||
const workspaces = useAtomValue(workspacesAtom);
|
||||
|
||||
return useMemo(() => getParents(folders, workspaces, m), [folders, workspaces, m]);
|
||||
}
|
||||
|
||||
function getParents(
|
||||
folders: Folder[],
|
||||
workspaces: Workspace[],
|
||||
currentModel: AnyModel | null,
|
||||
): ModelAncestor[] {
|
||||
if (currentModel == null) return [];
|
||||
|
||||
const parentFolder =
|
||||
'folderId' in currentModel && currentModel.folderId
|
||||
? folders.find((f) => f.id === currentModel.folderId)
|
||||
: null;
|
||||
|
||||
if (parentFolder != null) {
|
||||
return [parentFolder, ...getParents(folders, workspaces, parentFolder)];
|
||||
}
|
||||
|
||||
const parentWorkspace =
|
||||
'workspaceId' in currentModel && currentModel.workspaceId
|
||||
? workspaces.find((w) => w.id === currentModel.workspaceId)
|
||||
: null;
|
||||
|
||||
if (parentWorkspace != null) {
|
||||
return [parentWorkspace, ...getParents(folders, workspaces, parentWorkspace)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
||||
import { useFastMutation } from './useFastMutation';
|
||||
import { allRequestsAtom } from './useAllRequests';
|
||||
|
||||
export function useMoveToWorkspace(id: string) {
|
||||
return useFastMutation<void, unknown>({
|
||||
mutationKey: ['move_workspace', id],
|
||||
mutationFn: async () => {
|
||||
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
if (activeWorkspaceId == null) return;
|
||||
|
||||
const request = jotaiStore.get(allRequestsAtom).find((r) => r.id === id);
|
||||
if (request == null) return;
|
||||
|
||||
showDialog({
|
||||
id: 'change-workspace',
|
||||
title: 'Move Workspace',
|
||||
size: 'sm',
|
||||
render: ({ hide }) => (
|
||||
<MoveToWorkspaceDialog
|
||||
onDone={hide}
|
||||
request={request}
|
||||
activeWorkspaceId={activeWorkspaceId}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { getModel } from '@yaakapp-internal/models';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { getActiveCookieJar } from './useActiveCookieJar';
|
||||
import { getActiveEnvironment } from './useActiveEnvironment';
|
||||
import { useFastMutation } from './useFastMutation';
|
||||
import { createFastMutation, useFastMutation } from './useFastMutation';
|
||||
|
||||
export function useSendAnyHttpRequest() {
|
||||
return useFastMutation<HttpResponse | null, string, string | null>({
|
||||
@@ -22,3 +22,19 @@ export function useSendAnyHttpRequest() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const sendAnyHttpRequest = createFastMutation<HttpResponse | null, string, string | null>({
|
||||
mutationKey: ['send_any_request'],
|
||||
mutationFn: async (id) => {
|
||||
const request = getModel('http_request', id ?? 'n/a');
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return invokeCmd('cmd_send_http_request', {
|
||||
request,
|
||||
environmentId: getActiveEnvironment()?.id,
|
||||
cookieJarId: getActiveCookieJar()?.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,43 +1,29 @@
|
||||
import { keyValuesAtom } from '@yaakapp-internal/models';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { setKeyValue } from '../lib/keyValueStore';
|
||||
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
||||
import { getKeyValue } from './useKeyValue';
|
||||
|
||||
function kvKey(workspaceId: string | null) {
|
||||
return ['sidebar_collapsed', workspaceId ?? 'n/a'];
|
||||
}
|
||||
|
||||
export function useSidebarItemCollapsed(itemId: string) {
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(
|
||||
getSidebarCollapsedMap()[itemId] === true,
|
||||
);
|
||||
useEffect(
|
||||
() =>
|
||||
jotaiStore.sub(keyValuesAtom, () => {
|
||||
setIsCollapsed(getSidebarCollapsedMap()[itemId] === true);
|
||||
}),
|
||||
[itemId],
|
||||
);
|
||||
export const sidebarCollapsedAtom = atom((get) => {
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
return atomWithKVStorage<Record<string, boolean>>(kvKey(workspaceId), {});
|
||||
});
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setKeyValue({
|
||||
key: kvKey(jotaiStore.get(activeWorkspaceIdAtom)),
|
||||
namespace: 'no_sync',
|
||||
value: { ...getSidebarCollapsedMap(), [itemId]: !isCollapsed },
|
||||
}).catch(console.error);
|
||||
}, [isCollapsed, itemId]);
|
||||
export function useSidebarItemCollapsed(itemId: string) {
|
||||
const map = useAtomValue(useAtomValue(sidebarCollapsedAtom));
|
||||
const isCollapsed = map[itemId] === true;
|
||||
|
||||
const toggle = useCallback(() => toggleSidebarItemCollapsed(itemId), [itemId]);
|
||||
|
||||
return [isCollapsed, toggle] as const;
|
||||
}
|
||||
|
||||
export function getSidebarCollapsedMap() {
|
||||
const value = getKeyValue<Record<string, boolean>>({
|
||||
key: kvKey(jotaiStore.get(activeWorkspaceIdAtom)),
|
||||
fallback: {},
|
||||
namespace: 'no_sync',
|
||||
export function toggleSidebarItemCollapsed(itemId: string) {
|
||||
jotaiStore.set(jotaiStore.get(sidebarCollapsedAtom), (prev) => {
|
||||
return { ...prev, [itemId]: !prev[itemId] };
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user