From 0cb633e479acb2af232f146f0e47b38d6aaaff48 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 4 Nov 2025 08:44:08 -0800 Subject: [PATCH] A bunch of fixes --- src-tauri/src/notifications.rs | 2 +- src-web/components/EnvironmentEditDialog.tsx | 30 +- src-web/components/EnvironmentEditor.tsx | 2 +- .../components/Settings/SettingsGeneral.tsx | 1 - src-web/components/Sidebar.tsx | 110 ++++--- .../components/WorkspaceEncryptionSetting.tsx | 167 +++++----- src-web/components/core/tree/Tree.tsx | 310 ++++++------------ src-web/hooks/useEnvironmentsBreakdown.ts | 13 +- src-web/hooks/useRecentCookieJars.ts | 18 +- src-web/hooks/useRecentEnvironments.ts | 26 +- src-web/hooks/useRecentRequests.ts | 23 +- 11 files changed, 301 insertions(+), 401 deletions(-) diff --git a/src-tauri/src/notifications.rs b/src-tauri/src/notifications.rs index 95b0c94e..4991f5c0 100644 --- a/src-tauri/src/notifications.rs +++ b/src-tauri/src/notifications.rs @@ -86,7 +86,7 @@ impl YaakNotifier { #[cfg(feature = "license")] let license_check = { - use yaak_license::{check_license, LicenseCheckStatus}; + use yaak_license::{LicenseCheckStatus, check_license}; match check_license(window).await { Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(), Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(), diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index 6ec6c3be..2c1af24f 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -10,7 +10,7 @@ import { } from '../hooks/useEnvironmentsBreakdown'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { jotaiStore } from '../lib/jotai'; -import { isBaseEnvironment } from '../lib/model_util'; +import { isBaseEnvironment, isSubEnvironment } from '../lib/model_util'; import { resolvedModelName } from '../lib/resolvedModelName'; import { showColorPicker } from '../lib/showColorPicker'; import { Banner } from './core/Banner'; @@ -170,7 +170,18 @@ function EnvironmentEditDialogSidebar({ const getContextMenu = useCallback( (items: TreeModel[]): ContextMenuProps['items'] => { const environment = items[0]; - if (environment == null || environment.model !== 'environment') return []; + const addEnvironmentItem: DropdownItem = { + label: 'Create Sub Environment', + leftSlot: , + onSelect: async () => { + await createSubEnvironment(); + }, + }; + + if (environment == null || environment.model !== 'environment') { + return [addEnvironmentItem]; + } + const singleEnvironment = items.length === 1; const menuItems: DropdownItem[] = [ @@ -206,6 +217,7 @@ function EnvironmentEditDialogSidebar({ label: `Make ${environment.public ? 'Private' : 'Sharable'}`, leftSlot: , rightSlot: , + hidden: items.length > 1, onSelect: async () => { await patchModel(environment, { public: !environment.public }); }, @@ -215,22 +227,18 @@ function EnvironmentEditDialogSidebar({ label: 'Delete', hotKeyAction: 'sidebar.selected.delete', hotKeyLabelOnly: true, - hidden: !(isBaseEnvironment(environment) && baseEnvironments.length > 1), + hidden: + (isBaseEnvironment(environment) && baseEnvironments.length <= 1) || + !isSubEnvironment(environment), leftSlot: , onSelect: () => handleDeleteEnvironment(environment), }, ]; + // Add sub environment to base environment if (isBaseEnvironment(environment) && singleEnvironment) { menuItems.push({ type: 'separator' }); - menuItems.push({ - label: 'Create Sub Environment', - leftSlot: , - hidden: !isBaseEnvironment(environment), - onSelect: async () => { - await createSubEnvironment(); - }, - }); + menuItems.push(addEnvironmentItem); } return menuItems; diff --git a/src-web/components/EnvironmentEditor.tsx b/src-web/components/EnvironmentEditor.tsx index c9799f1b..3b8c2069 100644 --- a/src-web/components/EnvironmentEditor.tsx +++ b/src-web/components/EnvironmentEditor.tsx @@ -2,7 +2,7 @@ import type { Environment } from '@yaakapp-internal/models'; import { patchModel } from '@yaakapp-internal/models'; import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import classNames from 'classnames'; -import React, { useCallback, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled'; import { useKeyValue } from '../hooks/useKeyValue'; diff --git a/src-web/components/Settings/SettingsGeneral.tsx b/src-web/components/Settings/SettingsGeneral.tsx index 8f1ebfa3..3c904a60 100644 --- a/src-web/components/Settings/SettingsGeneral.tsx +++ b/src-web/components/Settings/SettingsGeneral.tsx @@ -1,7 +1,6 @@ import { revealItemInDir } from '@tauri-apps/plugin-opener'; import { patchModel, settingsAtom } from '@yaakapp-internal/models'; import { useAtomValue } from 'jotai'; -import React from 'react'; import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace'; import { appInfo } from '../../lib/appInfo'; import { useCheckForUpdates } from '../../hooks/useCheckForUpdates'; diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 0633f617..03ef6b5c 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -21,8 +21,7 @@ import { import classNames from 'classnames'; import { atom, useAtomValue } from 'jotai'; import { selectAtom } from 'jotai/utils'; -import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { useKey } from 'react-use'; +import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import { moveToWorkspace } from '../commands/moveToWorkspace'; import { openFolderSettings } from '../commands/openFolderSettings'; import { activeCookieJarAtom } from '../hooks/useActiveCookieJar'; @@ -150,33 +149,31 @@ function Sidebar({ className }: { className?: string }) { await Promise.all( items.map((m, i) => // Spread item sortPriority out over before/after range - patchModel(m, { sortPriority: beforePriority + (i + 1) * increment, folderId }), + patchModel(m, { + sortPriority: beforePriority + (i + 1) * increment, + folderId, + }), ), - ); - } + ); + op> + } } catch (e) { console.error(e); } }, []); - const handleTreeRefInit = useCallback((n: TreeHandle) => { - treeRef.current = n; - if (n == null) return; - const activeId = jotaiStore.get(activeIdAtom); - if (activeId == null) return; - const selectedIds = jotaiStore.get(selectedIdsFamily(treeId)); - if (selectedIds.length > 0) return; - n.selectItem(activeId); - }, [treeId]); - - // Ensure active id is always selected when it changes - useEffect(() => { - return jotaiStore.sub(activeIdAtom, () => { + const handleTreeRefInit = useCallback( + (n: TreeHandle) => { + treeRef.current = n; + if (n == null) return; const activeId = jotaiStore.get(activeIdAtom); if (activeId == null) return; - treeRef.current?.selectItem(activeId); - }); - }, []); + const selectedIds = jotaiStore.get(selectedIdsFamily(treeId)); + if (selectedIds.length > 0) return; + n.selectItem(activeId); + }, + [treeId], + ); const clearFilterText = useCallback(() => { jotaiStore.set(sidebarFilterAtom, { text: '', key: `${Math.random()}` }); @@ -204,14 +201,6 @@ function Sidebar({ className }: { className?: string }) { [], ); - // Focus the first sidebar item on arrow down from filter - useKey('ArrowDown', (e) => { - if (e.key === 'ArrowDown' && filterRef.current?.isFocused()) { - e.preventDefault(); - treeRef.current?.focus(); - } - }); - const actions = useMemo(() => { const enable = () => treeRef.current?.hasFocus() ?? false; @@ -291,7 +280,11 @@ function Sidebar({ className }: { className?: string }) { // No children means we're in the root if (child == null) { - return getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: null }); + return getCreateDropdownItems({ + workspaceId, + activeRequest: null, + folderId: null, + }); } const workspaces = jotaiStore.get(workspacesAtom); @@ -355,12 +348,19 @@ function Sidebar({ className }: { className?: string }) { items.length === 1 && child.model === 'folder' ? [ { type: 'separator' }, - ...getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: child.id }), + ...getCreateDropdownItems({ + workspaceId, + activeRequest: null, + folderId: child.id, + }), ] : []; const menuItems: ContextMenuProps['items'] = [ ...initialItems, - { type: 'separator', hidden: initialItems.filter((v) => !v.hidden).length === 0 }, + { + type: 'separator', + hidden: initialItems.filter((v) => !v.hidden).length === 0, + }, { label: 'Rename', leftSlot: , @@ -421,7 +421,9 @@ function Sidebar({ className }: { className?: string }) { const view = filterRef.current; if (!view) return; const ext = filter({ fields: allFields ?? [] }); - view.dispatch({ effects: filterLanguageCompartmentRef.current.reconfigure(ext) }); + view.dispatch({ + effects: filterLanguageCompartmentRef.current.reconfigure(ext), + }); }, [allFields]); if (tree == null || hidden) { @@ -434,7 +436,7 @@ function Sidebar({ className }: { className?: string }) { aria-hidden={hidden ?? undefined} className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)_auto]')} > -
+
{(tree.children?.length ?? 0) > 0 && ( <> ((get) => { const memoAllPotentialChildrenAtom = deepEqualAtom(allPotentialChildrenAtom); -const sidebarFilterAtom = atom<{ text: string; key: string }>({ text: '', key: '' }); +const sidebarFilterAtom = atom<{ text: string; key: string }>({ + text: '', + key: '', +}); const sidebarTreeAtom = atom<[TreeNode, FieldDef[]] | null>((get) => { const allModels = get(memoAllPotentialChildrenAtom); @@ -580,21 +585,24 @@ const sidebarTreeAtom = atom<[TreeNode, FieldDef[]] | null>((get) const build = (node: TreeNode, depth: number): boolean => { const childItems = childrenMap[node.item.id] ?? []; let matchesSelf = true; - const fields = getItemFields(node.item); + const fields = getItemFields(node); + const model = node.item.model; + const isLeafNode = !(model === 'folder' || model === 'workspace'); + for (const [field, value] of Object.entries(fields)) { if (!value) continue; allFields[field] = allFields[field] ?? new Set(); allFields[field].add(value); } + if (queryAst != null) { - matchesSelf = evaluate(queryAst, { text: getItemText(node.item), fields }); + matchesSelf = isLeafNode && evaluate(queryAst, { text: getItemText(node.item), fields }); } let matchesChild = false; // Recurse to children - const m = node.item.model; - node.children = m === 'folder' || m === 'workspace' ? [] : undefined; + node.children = !isLeafNode ? [] : undefined; if (node.children != null) { childItems.sort((a, b) => { @@ -623,7 +631,7 @@ const sidebarTreeAtom = atom<[TreeNode, FieldDef[]] | null>((get) const root: TreeNode = { item: activeWorkspace, - parent: null, + parent: null, children: [], depth: 0, }; @@ -633,7 +641,10 @@ const sidebarTreeAtom = atom<[TreeNode, FieldDef[]] | null>((get) const fields: FieldDef[] = []; for (const [name, values] of Object.entries(allFields)) { - fields.push({ name, values: Array.from(values).filter((v) => v.length < 20) }); + fields.push({ + name, + values: Array.from(values).filter((v) => v.length < 20), + }); } return [root, fields] as const; }); @@ -716,7 +727,9 @@ const SidebarInnerItem = memo(function SidebarInnerItem({ ); }); -function getItemFields(item: SidebarModel): Record { +function getItemFields(node: TreeNode): Record { + const item = node.item; + if (item.model === 'workspace') return {}; const fields: Record = {}; @@ -736,9 +749,20 @@ function getItemFields(item: SidebarModel): Record { if (item.model === 'grpc_request') fields.type = 'grpc'; else if (item.model === 'websocket_request') fields.type = 'ws'; + if (node.parent?.item.model === 'folder') { + fields.folder = node.parent.item.name; + } + return fields; } function getItemText(item: SidebarModel): string { - return resolvedModelName(item); + const segments = []; + if (item.model === 'http_request') { + segments.push(item.method); + } + + segments.push(resolvedModelName(item)); + + return segments.join(' '); } diff --git a/src-web/components/WorkspaceEncryptionSetting.tsx b/src-web/components/WorkspaceEncryptionSetting.tsx index 408ed911..9ff02dd7 100644 --- a/src-web/components/WorkspaceEncryptionSetting.tsx +++ b/src-web/components/WorkspaceEncryptionSetting.tsx @@ -1,49 +1,36 @@ -import { - enableEncryption, - revealWorkspaceKey, - setWorkspaceKey, -} from "@yaakapp-internal/crypto"; -import type { WorkspaceMeta } from "@yaakapp-internal/models"; -import classNames from "classnames"; -import { useAtomValue } from "jotai"; -import { useEffect, useState } from "react"; -import { - activeWorkspaceAtom, - activeWorkspaceMetaAtom, -} from "../hooks/useActiveWorkspace"; -import { createFastMutation } from "../hooks/useFastMutation"; -import { useStateWithDeps } from "../hooks/useStateWithDeps"; -import { CopyIconButton } from "./CopyIconButton"; -import { Banner } from "./core/Banner"; -import type { ButtonProps } from "./core/Button"; -import { Button } from "./core/Button"; -import { IconButton } from "./core/IconButton"; -import { IconTooltip } from "./core/IconTooltip"; -import { Label } from "./core/Label"; -import { PlainInput } from "./core/PlainInput"; -import { HStack, VStack } from "./core/Stacks"; -import { EncryptionHelp } from "./EncryptionHelp"; +import { enableEncryption, revealWorkspaceKey, setWorkspaceKey } from '@yaakapp-internal/crypto'; +import type { WorkspaceMeta } from '@yaakapp-internal/models'; +import classNames from 'classnames'; +import { useAtomValue } from 'jotai'; +import { useEffect, useState } from 'react'; +import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../hooks/useActiveWorkspace'; +import { createFastMutation } from '../hooks/useFastMutation'; +import { useStateWithDeps } from '../hooks/useStateWithDeps'; +import { CopyIconButton } from './CopyIconButton'; +import { Banner } from './core/Banner'; +import type { ButtonProps } from './core/Button'; +import { Button } from './core/Button'; +import { IconButton } from './core/IconButton'; +import { IconTooltip } from './core/IconTooltip'; +import { Label } from './core/Label'; +import { PlainInput } from './core/PlainInput'; +import { HStack, VStack } from './core/Stacks'; +import { EncryptionHelp } from './EncryptionHelp'; interface Props { - size?: ButtonProps["size"]; + size?: ButtonProps['size']; expanded?: boolean; onDone?: () => void; onEnabledEncryption?: () => void; } -export function WorkspaceEncryptionSetting( - { size, expanded, onDone, onEnabledEncryption }: Props, -) { - const [justEnabledEncryption, setJustEnabledEncryption] = useState( - false, - ); +export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEncryption }: Props) { + const [justEnabledEncryption, setJustEnabledEncryption] = useState(false); const [error, setError] = useState(null); const workspace = useAtomValue(activeWorkspaceAtom); const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom); - const [key, setKey] = useState< - { key: string | null; error: string | null } | null - >(null); + const [key, setKey] = useState<{ key: string | null; error: string | null } | null>(null); useEffect(() => { if (workspaceMeta == null) { @@ -122,7 +109,7 @@ export function WorkspaceEncryptionSetting( return (
- {error && {error}} - {expanded - ? ( - - - - ) - : ( - - )} + {error && ( + + {error} + + )} + {expanded ? ( + + + + ) : ( + + )}
); } const setWorkspaceKeyMut = createFastMutation({ - mutationKey: ["set-workspace-key"], + mutationKey: ['set-workspace-key'], mutationFn: setWorkspaceKey, }); @@ -166,13 +155,15 @@ function EnterWorkspaceKey({ onEnabled?: () => void; error?: string | null; }) { - const [key, setKey] = useState(""); + const [key, setKey] = useState(''); return ( - {error ? {error} : ( + {error ? ( + {error} + ) : ( - This workspace contains encrypted values but no key is configured. - Please enter the workspace key to access the encrypted data. + This workspace contains encrypted values but no key is configured. Please enter the + workspace key to access the encrypted data. )} {!disableLabel && ( - Workspace encryption key{" "} - + Workspace encryption key{' '} + )} - {encryptionKey && ( - - )} + {encryptionKey && } - {encryptionKey && ( - - )} + {encryptionKey && } setShow((v) => !v)} /> @@ -258,32 +238,31 @@ function KeyRevealer({ function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) { return ( - {show - ? ( - keyText.split("").map((c, i) => { - return ( - - {c} - - ); - }) - ) - :
•••••••••••••••••••••
} + {show ? ( + keyText.split('').map((c, i) => { + return ( + + {c} + + ); + }) + ) : ( +
•••••••••••••••••••••
+ )}
); } const helpAfterEncryption = (

- The following key is used for encryption operations within this workspace. - It is stored securely using your OS keychain, but it is recommended to back - it up. If you share this workspace with others, you'll need to send - them this key to access any encrypted values. + The following key is used for encryption operations within this workspace. It is stored securely + using your OS keychain, but it is recommended to back it up. If you share this workspace with + others, you'll need to send them this key to access any encrypted values.

); diff --git a/src-web/components/core/tree/Tree.tsx b/src-web/components/core/tree/Tree.tsx index 38672d6b..5ae2826c 100644 --- a/src-web/components/core/tree/Tree.tsx +++ b/src-web/components/core/tree/Tree.tsx @@ -1,8 +1,4 @@ -import type { - DragEndEvent, - DragMoveEvent, - DragStartEvent, -} from "@dnd-kit/core"; +import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core'; import { DndContext, MeasuringStrategy, @@ -11,16 +7,10 @@ import { useDroppable, useSensor, useSensors, -} from "@dnd-kit/core"; -import { type } from "@tauri-apps/plugin-os"; -import classNames from "classnames"; -import type { - ComponentType, - MouseEvent, - ReactElement, - Ref, - RefAttributes, -} from "react"; +} from '@dnd-kit/core'; +import { type } from '@tauri-apps/plugin-os'; +import classNames from 'classnames'; +import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react'; import { forwardRef, memo, @@ -30,14 +20,14 @@ import { useMemo, useRef, useState, -} from "react"; -import { useKey, useKeyPressEvent } from "react-use"; -import type { HotkeyAction, HotKeyOptions } from "../../../hooks/useHotKey"; -import { useHotKey } from "../../../hooks/useHotKey"; -import { computeSideForDragMove } from "../../../lib/dnd"; -import { jotaiStore } from "../../../lib/jotai"; -import type { ContextMenuProps, DropdownItem } from "../Dropdown"; -import { ContextMenu } from "../Dropdown"; +} from 'react'; +import { useKey, useKeyPressEvent } from 'react-use'; +import type { HotkeyAction, HotKeyOptions } from '../../../hooks/useHotKey'; +import { useHotKey } from '../../../hooks/useHotKey'; +import { computeSideForDragMove } from '../../../lib/dnd'; +import { jotaiStore } from '../../../lib/jotai'; +import type { ContextMenuProps, DropdownItem } from '../Dropdown'; +import { ContextMenu } from '../Dropdown'; import { collapsedFamily, draggingIdsFamily, @@ -45,23 +35,14 @@ import { hoveredParentFamily, isCollapsedFamily, selectedIdsFamily, -} from "./atoms"; -import type { SelectableTreeNode, TreeNode } from "./common"; -import { - closestVisibleNode, - equalSubtree, - getSelectedItems, - hasAncestor, -} from "./common"; -import { TreeDragOverlay } from "./TreeDragOverlay"; -import type { - TreeItemClickEvent, - TreeItemHandle, - TreeItemProps, -} from "./TreeItem"; -import type { TreeItemListProps } from "./TreeItemList"; -import { TreeItemList } from "./TreeItemList"; -import { useSelectableItems } from "./useSelectableItems"; +} from './atoms'; +import type { SelectableTreeNode, TreeNode } from './common'; +import { closestVisibleNode, equalSubtree, getSelectedItems, hasAncestor } from './common'; +import { TreeDragOverlay } from './TreeDragOverlay'; +import type { TreeItemClickEvent, TreeItemHandle, TreeItemProps } from './TreeItem'; +import type { TreeItemListProps } from './TreeItemList'; +import { TreeItemList } from './TreeItemList'; +import { useSelectableItems } from './useSelectableItems'; /** So we re-calculate after expanding a folder during drag */ const measuring = { droppable: { strategy: MeasuringStrategy.Always } }; @@ -70,21 +51,15 @@ export interface TreeProps { root: TreeNode; treeId: string; getItemKey: (item: T) => string; - getContextMenu?: ( - items: T[], - ) => ContextMenuProps["items"] | Promise; + getContextMenu?: (items: T[]) => ContextMenuProps['items'] | Promise; ItemInner: ComponentType<{ treeId: string; item: T }>; ItemLeftSlotInner?: ComponentType<{ treeId: string; item: T }>; ItemRightSlot?: ComponentType<{ treeId: string; item: T }>; className?: string; onActivate?: (item: T) => void; - onDragEnd?: ( - opt: { items: T[]; parent: T; children: T[]; insertAt: number }, - ) => void; + onDragEnd?: (opt: { items: T[]; parent: T; children: T[]; insertAt: number }) => void; hotkeys?: { - actions: Partial< - Record void } & HotKeyOptions> - >; + actions: Partial void } & HotKeyOptions>>; }; getEditOptions?: (item: T) => { defaultValue: string; @@ -121,24 +96,19 @@ function TreeInner( ) { const treeRef = useRef(null); const selectableItems = useSelectableItems(root); - const [showContextMenu, setShowContextMenu] = useState< - { - items: DropdownItem[]; - x: number; - y: number; - } | null - >(null); + const [showContextMenu, setShowContextMenu] = useState<{ + items: DropdownItem[]; + x: number; + y: number; + } | null>(null); const treeItemRefs = useRef>({}); - const handleAddTreeItemRef = useCallback( - (item: T, r: TreeItemHandle | null) => { - if (r == null) { - delete treeItemRefs.current[item.id]; - } else { - treeItemRefs.current[item.id] = r; - } - }, - [], - ); + const handleAddTreeItemRef = useCallback((item: T, r: TreeItemHandle | null) => { + if (r == null) { + delete treeItemRefs.current[item.id]; + } else { + treeItemRefs.current[item.id] = r; + } + }, []); // Select the first item on first render useEffect(() => { @@ -176,8 +146,8 @@ function TreeInner( const ensureTabbableItem = useCallback(() => { const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId; - const lastSelectedItem = selectableItems.find((i) => - i.node.item.id === lastSelectedId && !i.node.hidden + const lastSelectedItem = selectableItems.find( + (i) => i.node.item.id === lastSelectedId && !i.node.hidden, ); // If no item found, default to selecting the first item (prefer leaf node); @@ -224,8 +194,7 @@ function TreeInner( () => ({ treeId, focus: tryFocus, - hasFocus: () => - treeRef.current?.contains(document.activeElement) ?? false, + hasFocus: () => treeRef.current?.contains(document.activeElement) ?? false, renameItem: (id) => treeItemRefs.current[id]?.rename(), selectItem: (id) => { if (jotaiStore.get(selectedIdsFamily(treeId)).includes(id)) { @@ -240,9 +209,7 @@ function TreeInner( const items = getSelectedItems(treeId, selectableItems); const menuItems = await getContextMenu(items); const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId; - const rect = lastSelectedId - ? treeItemRefs.current[lastSelectedId]?.rect() - : null; + const rect = lastSelectedId ? treeItemRefs.current[lastSelectedId]?.rect() : null; if (rect == null) return; setShowContextMenu({ items: menuItems, x: rect.x, y: rect.y }); }, @@ -264,66 +231,43 @@ function TreeInner( // If right-clicked an item that was NOT in the multiple-selection, just use that one // Also update the selection with it setSelected([item.id], false); - jotaiStore.set( - focusIdsFamily(treeId), - (prev) => ({ ...prev, lastId: item.id }), - ); + jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id })); return getContextMenu([item]); } }; }, [getContextMenu, selectableItems, setSelected, treeId]); - const handleSelect = useCallback["onClick"]>>( + const handleSelect = useCallback['onClick']>>( (item, { shiftKey, metaKey, ctrlKey }) => { const anchorSelectedId = jotaiStore.get(focusIdsFamily(treeId)).anchorId; const selectedIdsAtom = selectedIdsFamily(treeId); const selectedIds = jotaiStore.get(selectedIdsAtom); // Mark the item as the last one selected - jotaiStore.set( - focusIdsFamily(treeId), - (prev) => ({ ...prev, lastId: item.id }), - ); + jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id })); if (shiftKey) { - const anchorIndex = selectableItems.findIndex((i) => - i.node.item.id === anchorSelectedId - ); - const currIndex = selectableItems.findIndex((v) => - v.node.item.id === item.id - ); + const validSelectableItems = getValidSelectableItems(treeId, selectableItems); + const anchorIndex = validSelectableItems.findIndex((i) => i.node.item.id === anchorSelectedId); + const currIndex = validSelectableItems.findIndex((v) => v.node.item.id === item.id); + // Nothing was selected yet, so just select this item - if ( - selectedIds.length === 0 || anchorIndex === -1 || currIndex === -1 - ) { + if (selectedIds.length === 0 || anchorIndex === -1 || currIndex === -1) { setSelected([item.id], true); - jotaiStore.set( - focusIdsFamily(treeId), - (prev) => ({ ...prev, anchorId: item.id }), - ); + jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, anchorId: item.id })); return; } - const validSelectableItems = getValidSelectableItems( - treeId, - selectableItems, - ); if (currIndex > anchorIndex) { // Selecting down - const itemsToSelect = validSelectableItems.slice( - anchorIndex, - currIndex + 1, - ); + const itemsToSelect = validSelectableItems.slice(anchorIndex, currIndex + 1); setSelected( itemsToSelect.map((v) => v.node.item.id), true, ); } else if (currIndex < anchorIndex) { // Selecting up - const itemsToSelect = validSelectableItems.slice( - currIndex, - anchorIndex + 1, - ); + const itemsToSelect = validSelectableItems.slice(currIndex, anchorIndex + 1); setSelected( itemsToSelect.map((v) => v.node.item.id), true, @@ -331,7 +275,7 @@ function TreeInner( } else { setSelected([item.id], true); } - } else if (type() === "macos" ? metaKey : ctrlKey) { + } else if (type() === 'macos' ? metaKey : ctrlKey) { const withoutCurr = selectedIds.filter((id) => id !== item.id); if (withoutCurr.length === selectedIds.length) { // It wasn't in there, so add it @@ -343,16 +287,13 @@ function TreeInner( } else { // Select single setSelected([item.id], true); - jotaiStore.set( - focusIdsFamily(treeId), - (prev) => ({ ...prev, anchorId: item.id }), - ); + jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, anchorId: item.id })); } }, [selectableItems, setSelected, treeId], ); - const handleClick = useCallback["onClick"]>>( + const handleClick = useCallback['onClick']>>( (item, e) => { if (e.shiftKey || e.ctrlKey || e.metaKey) { handleSelect(item, e); @@ -367,13 +308,8 @@ function TreeInner( const selectPrevItem = useCallback( (e: TreeItemClickEvent) => { const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId; - const validSelectableItems = getValidSelectableItems( - treeId, - selectableItems, - ); - const index = validSelectableItems.findIndex((i) => - i.node.item.id === lastSelectedId - ); + const validSelectableItems = getValidSelectableItems(treeId, selectableItems); + const index = validSelectableItems.findIndex((i) => i.node.item.id === lastSelectedId); const item = validSelectableItems[index - 1]; if (item != null) { handleSelect(item.node.item, e); @@ -385,13 +321,8 @@ function TreeInner( const selectNextItem = useCallback( (e: TreeItemClickEvent) => { const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId; - const validSelectableItems = getValidSelectableItems( - treeId, - selectableItems, - ); - const index = validSelectableItems.findIndex((i) => - i.node.item.id === lastSelectedId - ); + const validSelectableItems = getValidSelectableItems(treeId, selectableItems); + const index = validSelectableItems.findIndex((i) => i.node.item.id === lastSelectedId); const item = validSelectableItems[index + 1]; if (item != null) { handleSelect(item.node.item, e); @@ -404,8 +335,7 @@ function TreeInner( (e: TreeItemClickEvent) => { const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId; const lastSelectedItem = - selectableItems.find((i) => i.node.item.id === lastSelectedId)?.node ?? - null; + selectableItems.find((i) => i.node.item.id === lastSelectedId)?.node ?? null; if (lastSelectedItem?.parent != null) { handleSelect(lastSelectedItem.parent.item, e); } @@ -414,7 +344,7 @@ function TreeInner( ); useKey( - (e) => e.key === "ArrowUp" || e.key.toLowerCase() === "k", + (e) => e.key === 'ArrowUp' || e.key.toLowerCase() === 'k', (e) => { if (!isTreeFocused()) return; e.preventDefault(); @@ -425,7 +355,7 @@ function TreeInner( ); useKey( - (e) => e.key === "ArrowDown" || e.key.toLowerCase() === "j", + (e) => e.key === 'ArrowDown' || e.key.toLowerCase() === 'j', (e) => { if (!isTreeFocused()) return; e.preventDefault(); @@ -437,26 +367,21 @@ function TreeInner( // If the selected item is a collapsed folder, expand it. Otherwise, select next item useKey( - (e) => e.key === "ArrowRight" || e.key === "l", + (e) => e.key === 'ArrowRight' || e.key === 'l', (e) => { if (!isTreeFocused()) return; e.preventDefault(); const collapsed = jotaiStore.get(collapsedFamily(treeId)); const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId; - const lastSelectedItem = selectableItems.find((i) => - i.node.item.id === lastSelectedId - ); + const lastSelectedItem = selectableItems.find((i) => i.node.item.id === lastSelectedId); if ( lastSelectedId && lastSelectedItem?.node.children != null && collapsed[lastSelectedItem.node.item.id] === true ) { - jotaiStore.set( - isCollapsedFamily({ treeId, itemId: lastSelectedId }), - false, - ); + jotaiStore.set(isCollapsedFamily({ treeId, itemId: lastSelectedId }), false); } else { selectNextItem(e); } @@ -468,26 +393,21 @@ function TreeInner( // If the selected item is in a folder, select its parent. // If the selected item is an expanded folder, collapse it. useKey( - (e) => e.key === "ArrowLeft" || e.key === "h", + (e) => e.key === 'ArrowLeft' || e.key === 'h', (e) => { if (!isTreeFocused()) return; e.preventDefault(); const collapsed = jotaiStore.get(collapsedFamily(treeId)); const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId; - const lastSelectedItem = selectableItems.find((i) => - i.node.item.id === lastSelectedId - ); + const lastSelectedItem = selectableItems.find((i) => i.node.item.id === lastSelectedId); if ( lastSelectedId && lastSelectedItem?.node.children != null && collapsed[lastSelectedItem.node.item.id] !== true ) { - jotaiStore.set( - isCollapsedFamily({ treeId, itemId: lastSelectedId }), - true, - ); + jotaiStore.set(isCollapsedFamily({ treeId, itemId: lastSelectedId }), true); } else { selectParentItem(e); } @@ -496,7 +416,7 @@ function TreeInner( [selectableItems, handleSelect], ); - useKeyPressEvent("Escape", async () => { + useKeyPressEvent('Escape', async () => { if (!treeRef.current?.contains(document.activeElement)) return; clearDragState(); const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId; @@ -535,22 +455,19 @@ function TreeInner( return; } - const overSelectableItem = - selectableItems.find((i) => i.node.item.id === over.id) ?? null; + const overSelectableItem = selectableItems.find((i) => i.node.item.id === over.id) ?? null; if (overSelectableItem == null) { return; } const draggingItems = jotaiStore.get(draggingIdsFamily(treeId)); for (const id of draggingItems) { - const item = selectableItems.find((i) => i.node.item.id === id)?.node ?? - null; + const item = selectableItems.find((i) => i.node.item.id === id)?.node ?? null; if (item == null) { return; } - const isSameParent = - item.parent?.item.id === overSelectableItem.node.parent?.item.id; + const isSameParent = item.parent?.item.id === overSelectableItem.node.parent?.item.id; if (item.localDrag && !isSameParent) { return; } @@ -561,15 +478,13 @@ function TreeInner( const item = node.item; let hoveredParent = node.parent; - const dragIndex = - selectableItems.findIndex((n) => n.node.item.id === item.id) ?? -1; + const dragIndex = selectableItems.findIndex((n) => n.node.item.id === item.id) ?? -1; const hovered = selectableItems[dragIndex]?.node ?? null; - const hoveredIndex = dragIndex + (side === "above" ? 0 : 1); - let hoveredChildIndex = overSelectableItem.index + - (side === "above" ? 0 : 1); + const hoveredIndex = dragIndex + (side === 'above' ? 0 : 1); + let hoveredChildIndex = overSelectableItem.index + (side === 'above' ? 0 : 1); // Move into the folder if it's open and we're moving below it - if (hovered?.children != null && side === "below") { + if (hovered?.children != null && side === 'below') { hoveredParent = hovered; hoveredChildIndex = 0; } @@ -601,9 +516,7 @@ function TreeInner( const handleDragStart = useCallback( function handleDragStart(e: DragStartEvent) { const selectedItems = getSelectedItems(treeId, selectableItems); - const isDraggingSelectedItem = selectedItems.find((i) => - i.id === e.active.id - ); + const isDraggingSelectedItem = selectedItems.find((i) => i.id === e.active.id); // If we started dragging an already-selected item, we'll use that if (isDraggingSelectedItem) { @@ -613,9 +526,7 @@ function TreeInner( ); } else { // If we started dragging a non-selected item, only drag that item - const activeItem = selectableItems.find((i) => - i.node.item.id === e.active.id - )?.node.item; + const activeItem = selectableItems.find((i) => i.node.item.id === e.active.id)?.node.item; if (activeItem != null) { jotaiStore.set(draggingIdsFamily(treeId), [activeItem.id]); // Also update selection to just be this one @@ -656,30 +567,25 @@ function TreeInner( return; } - const hoveredParentS = hoveredParentId === root.item.id - ? { node: root, depth: 0, index: 0 } - : (selectableItems.find((i) => i.node.item.id === hoveredParentId) ?? - null); + const hoveredParentS = + hoveredParentId === root.item.id + ? { node: root, depth: 0, index: 0 } + : (selectableItems.find((i) => i.node.item.id === hoveredParentId) ?? null); const hoveredParent = hoveredParentS?.node ?? null; - if ( - hoveredParent == null || hoveredIndex == null || !draggingItems?.length - ) { + if (hoveredParent == null || hoveredIndex == null || !draggingItems?.length) { return; } // Resolve the actual tree nodes for each dragged item (keeps order of draggingItems) const draggedNodes: TreeNode[] = draggingItems .map((id) => { - return selectableItems.find((i) => i.node.item.id === id)?.node ?? - null; + return selectableItems.find((i) => i.node.item.id === id)?.node ?? null; }) .filter((n) => n != null) // Filter out invalid drags (dragging into descendant) .filter( - (n) => - hoveredParent.item.id !== n.item.id && - !hasAncestor(hoveredParent, n.item.id), + (n) => hoveredParent.item.id !== n.item.id && !hasAncestor(hoveredParent, n.item.id), ); // Work on a local copy of target children @@ -708,7 +614,7 @@ function TreeInner( const treeItemListProps: Omit< TreeItemListProps, - "nodes" | "treeId" | "activeIdAtom" | "hoveredParent" | "hoveredIndex" + 'nodes' | 'treeId' | 'activeIdAtom' | 'hoveredParent' | 'hoveredIndex' > = { getItemKey, getContextMenu: handleGetContextMenu, @@ -731,17 +637,11 @@ function TreeInner( [getContextMenu], ); - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), - ); + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } })); return ( <> - + {showContextMenu && ( ( ref={treeRef} className={classNames( className, - "outline-none h-full", - "overflow-y-auto overflow-x-hidden", - "grid grid-rows-[auto_1fr]", + 'outline-none h-full', + 'overflow-y-auto overflow-x-hidden', + 'grid grid-rows-[auto_1fr]', )} >
( />
{/* Assign root ID so we can reuse our same move/end logic */} - +
{ for (const key of Object.keys(prevProps)) { - if ( - prevProps[key as keyof typeof prevProps] !== - nextProps[key as keyof typeof nextProps] - ) { + if (prevProps[key as keyof typeof prevProps] !== nextProps[key as keyof typeof nextProps]) { return false; } } @@ -864,7 +758,7 @@ function TreeHotKey({ ...options, enable: () => { if (enable == null) return true; - if (typeof enable === "function") return enable(); + if (typeof enable === 'function') return enable(); else return enable; }, }, @@ -878,7 +772,7 @@ function TreeHotKeys({ selectableItems, }: { treeId: string; - hotkeys: TreeProps["hotkeys"]; + hotkeys: TreeProps['hotkeys']; selectableItems: SelectableTreeNode[]; }) { if (hotkeys == null) return null; diff --git a/src-web/hooks/useEnvironmentsBreakdown.ts b/src-web/hooks/useEnvironmentsBreakdown.ts index 8ad84e2b..e510a637 100644 --- a/src-web/hooks/useEnvironmentsBreakdown.ts +++ b/src-web/hooks/useEnvironmentsBreakdown.ts @@ -4,7 +4,18 @@ import { atom, useAtomValue } from 'jotai'; export const environmentsBreakdownAtom = atom((get) => { const allEnvironments = get(environmentsAtom); const baseEnvironments = allEnvironments.filter((e) => e.parentModel === 'workspace') ?? []; - const subEnvironments = allEnvironments.filter((e) => e.parentModel === 'environment') ?? []; + + const subEnvironments = + allEnvironments + .filter((e) => e.parentModel === 'environment') + ?.sort((a, b) => { + if (a.sortPriority === b.sortPriority) { + return a.updatedAt > b.updatedAt ? 1 : -1; + } else { + return a.sortPriority - b.sortPriority; + } + }) ?? []; + const folderEnvironments = allEnvironments.filter((e) => e.parentModel === 'folder' && e.parentId != null) ?? []; diff --git a/src-web/hooks/useRecentCookieJars.ts b/src-web/hooks/useRecentCookieJars.ts index d3662b4f..af9a5825 100644 --- a/src-web/hooks/useRecentCookieJars.ts +++ b/src-web/hooks/useRecentCookieJars.ts @@ -4,7 +4,6 @@ import { useEffect, useMemo } from 'react'; import { jotaiStore } from '../lib/jotai'; import { getKeyValue, setKeyValue } from '../lib/keyValueStore'; import { activeCookieJarAtom } from './useActiveCookieJar'; -import { activeWorkspaceIdAtom } from './useActiveWorkspace'; import { useKeyValue } from './useKeyValue'; const kvKey = (workspaceId: string) => 'recent_cookie_jars::' + workspaceId; @@ -13,9 +12,8 @@ const fallback: string[] = []; export function useRecentCookieJars() { const cookieJars = useAtomValue(cookieJarsAtom); - const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom); const kv = useKeyValue({ - key: kvKey(activeWorkspaceId ?? 'n/a'), + key: kvKey(cookieJars[0]?.workspaceId ?? 'n/a'), namespace, fallback, }); @@ -31,18 +29,16 @@ export function useRecentCookieJars() { export function useSubscribeRecentCookieJars() { useEffect(() => { return jotaiStore.sub(activeCookieJarAtom, async () => { - const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom); - const activeCookieJarId = jotaiStore.get(activeCookieJarAtom)?.id ?? null; - if (activeWorkspaceId == null) return; - if (activeCookieJarId == null) return; + const activeCookieJar = jotaiStore.get(activeCookieJarAtom); + if (activeCookieJar == null) return; - const key = kvKey(activeWorkspaceId); + const key = kvKey(activeCookieJar.workspaceId); const recentIds = getKeyValue({ namespace, key, fallback }); - if (recentIds[0] === activeCookieJarId) return; // Short-circuit + if (recentIds[0] === activeCookieJar.id) return; // Short-circuit - const withoutActiveId = recentIds.filter((id) => id !== activeCookieJarId); - const value = [activeCookieJarId, ...withoutActiveId]; + const withoutActiveId = recentIds.filter((id) => id !== activeCookieJar.id); + const value = [activeCookieJar.id, ...withoutActiveId]; await setKeyValue({ namespace, key, value }); }); }, []); diff --git a/src-web/hooks/useRecentEnvironments.ts b/src-web/hooks/useRecentEnvironments.ts index 2e7f9f1c..ea4a88bf 100644 --- a/src-web/hooks/useRecentEnvironments.ts +++ b/src-web/hooks/useRecentEnvironments.ts @@ -1,9 +1,7 @@ -import { useAtomValue } from 'jotai'; import { useEffect, useMemo } from 'react'; import { jotaiStore } from '../lib/jotai'; import { getKeyValue, setKeyValue } from '../lib/keyValueStore'; -import { activeEnvironmentIdAtom } from './useActiveEnvironment'; -import { activeWorkspaceAtom, activeWorkspaceIdAtom } from './useActiveWorkspace'; +import { activeEnvironmentAtom } from './useActiveEnvironment'; import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown'; import { useKeyValue } from './useKeyValue'; @@ -12,10 +10,9 @@ const namespace = 'global'; const fallback: string[] = []; export function useRecentEnvironments() { - const { subEnvironments } = useEnvironmentsBreakdown(); - const activeWorkspace = useAtomValue(activeWorkspaceAtom); + const { subEnvironments, allEnvironments } = useEnvironmentsBreakdown(); const kv = useKeyValue({ - key: kvKey(activeWorkspace?.id ?? 'n/a'), + key: kvKey(allEnvironments[0]?.workspaceId ?? 'n/a'), namespace, fallback, }); @@ -30,19 +27,16 @@ export function useRecentEnvironments() { export function useSubscribeRecentEnvironments() { useEffect(() => { - return jotaiStore.sub(activeEnvironmentIdAtom, async () => { - const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom); - const activeEnvironmentId = jotaiStore.get(activeEnvironmentIdAtom); - if (activeWorkspaceId == null) return; - if (activeEnvironmentId == null) return; - - const key = kvKey(activeWorkspaceId); + return jotaiStore.sub(activeEnvironmentAtom, async () => { + const activeEnvironment = jotaiStore.get(activeEnvironmentAtom); + if (activeEnvironment == null) return; + const key = kvKey(activeEnvironment.workspaceId); const recentIds = getKeyValue({ namespace, key, fallback }); - if (recentIds[0] === activeEnvironmentId) return; // Short-circuit + if (recentIds[0] === activeEnvironment.id) return; // Short-circuit - const withoutActiveId = recentIds.filter((id) => id !== activeEnvironmentId); - const value = [activeEnvironmentId, ...withoutActiveId]; + const withoutActiveId = recentIds.filter((id) => id !== activeEnvironment.id); + const value = [activeEnvironment.id, ...withoutActiveId]; await setKeyValue({ namespace, key, value }); }); }, []); diff --git a/src-web/hooks/useRecentRequests.ts b/src-web/hooks/useRecentRequests.ts index 87bac6fc..9d0af70c 100644 --- a/src-web/hooks/useRecentRequests.ts +++ b/src-web/hooks/useRecentRequests.ts @@ -1,11 +1,9 @@ -import { useAtomValue } from 'jotai'; import { useEffect, useMemo } from 'react'; import { jotaiStore } from '../lib/jotai'; import { getKeyValue, setKeyValue } from '../lib/keyValueStore'; -import { activeRequestIdAtom } from './useActiveRequestId'; -import { activeWorkspaceIdAtom } from './useActiveWorkspace'; import { useKeyValue } from './useKeyValue'; import { useAllRequests } from './useAllRequests'; +import { activeRequestAtom } from './useActiveRequest'; const kvKey = (workspaceId: string) => 'recent_requests::' + workspaceId; const namespace = 'global'; @@ -13,10 +11,9 @@ const fallback: string[] = []; export function useRecentRequests() { const requests = useAllRequests(); - const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom); const { set: setRecentRequests, value: recentRequests } = useKeyValue({ - key: kvKey(activeWorkspaceId ?? 'n/a'), + key: kvKey(requests[0]?.workspaceId ?? 'n/a'), namespace, fallback, }); @@ -31,19 +28,17 @@ export function useRecentRequests() { export function useSubscribeRecentRequests() { useEffect(() => { - return jotaiStore.sub(activeRequestIdAtom, async () => { - const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom); - const activeRequestId = jotaiStore.get(activeRequestIdAtom); - if (activeWorkspaceId == null) return; - if (activeRequestId == null) return; + return jotaiStore.sub(activeRequestAtom, async () => { + const activeRequest = jotaiStore.get(activeRequestAtom); + if (activeRequest == null) return; - const key = kvKey(activeWorkspaceId); + const key = kvKey(activeRequest.workspaceId); const recentIds = getKeyValue({ namespace, key, fallback }); - if (recentIds[0] === activeRequestId) return; // Short-circuit + if (recentIds[0] === activeRequest.id) return; // Short-circuit - const withoutActiveId = recentIds.filter((id) => id !== activeRequestId); - const value = [activeRequestId, ...withoutActiveId]; + const withoutActiveId = recentIds.filter((id) => id !== activeRequest.id); + const value = [activeRequest.id, ...withoutActiveId]; await setKeyValue({ namespace, key, value }); }); }, []);