mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-20 07:41:22 +02:00
A bunch of fixes
This commit is contained in:
@@ -86,7 +86,7 @@ impl YaakNotifier {
|
|||||||
|
|
||||||
#[cfg(feature = "license")]
|
#[cfg(feature = "license")]
|
||||||
let license_check = {
|
let license_check = {
|
||||||
use yaak_license::{check_license, LicenseCheckStatus};
|
use yaak_license::{LicenseCheckStatus, check_license};
|
||||||
match check_license(window).await {
|
match check_license(window).await {
|
||||||
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(),
|
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(),
|
||||||
Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(),
|
Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '../hooks/useEnvironmentsBreakdown';
|
} from '../hooks/useEnvironmentsBreakdown';
|
||||||
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
||||||
import { jotaiStore } from '../lib/jotai';
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { isBaseEnvironment } from '../lib/model_util';
|
import { isBaseEnvironment, isSubEnvironment } from '../lib/model_util';
|
||||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||||
import { showColorPicker } from '../lib/showColorPicker';
|
import { showColorPicker } from '../lib/showColorPicker';
|
||||||
import { Banner } from './core/Banner';
|
import { Banner } from './core/Banner';
|
||||||
@@ -170,7 +170,18 @@ function EnvironmentEditDialogSidebar({
|
|||||||
const getContextMenu = useCallback(
|
const getContextMenu = useCallback(
|
||||||
(items: TreeModel[]): ContextMenuProps['items'] => {
|
(items: TreeModel[]): ContextMenuProps['items'] => {
|
||||||
const environment = items[0];
|
const environment = items[0];
|
||||||
if (environment == null || environment.model !== 'environment') return [];
|
const addEnvironmentItem: DropdownItem = {
|
||||||
|
label: 'Create Sub Environment',
|
||||||
|
leftSlot: <Icon icon="plus" />,
|
||||||
|
onSelect: async () => {
|
||||||
|
await createSubEnvironment();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (environment == null || environment.model !== 'environment') {
|
||||||
|
return [addEnvironmentItem];
|
||||||
|
}
|
||||||
|
|
||||||
const singleEnvironment = items.length === 1;
|
const singleEnvironment = items.length === 1;
|
||||||
|
|
||||||
const menuItems: DropdownItem[] = [
|
const menuItems: DropdownItem[] = [
|
||||||
@@ -206,6 +217,7 @@ function EnvironmentEditDialogSidebar({
|
|||||||
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
|
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
|
||||||
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,
|
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,
|
||||||
rightSlot: <EnvironmentSharableTooltip />,
|
rightSlot: <EnvironmentSharableTooltip />,
|
||||||
|
hidden: items.length > 1,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
await patchModel(environment, { public: !environment.public });
|
await patchModel(environment, { public: !environment.public });
|
||||||
},
|
},
|
||||||
@@ -215,22 +227,18 @@ function EnvironmentEditDialogSidebar({
|
|||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
hotKeyAction: 'sidebar.selected.delete',
|
hotKeyAction: 'sidebar.selected.delete',
|
||||||
hotKeyLabelOnly: true,
|
hotKeyLabelOnly: true,
|
||||||
hidden: !(isBaseEnvironment(environment) && baseEnvironments.length > 1),
|
hidden:
|
||||||
|
(isBaseEnvironment(environment) && baseEnvironments.length <= 1) ||
|
||||||
|
!isSubEnvironment(environment),
|
||||||
leftSlot: <Icon icon="trash" />,
|
leftSlot: <Icon icon="trash" />,
|
||||||
onSelect: () => handleDeleteEnvironment(environment),
|
onSelect: () => handleDeleteEnvironment(environment),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add sub environment to base environment
|
||||||
if (isBaseEnvironment(environment) && singleEnvironment) {
|
if (isBaseEnvironment(environment) && singleEnvironment) {
|
||||||
menuItems.push({ type: 'separator' });
|
menuItems.push({ type: 'separator' });
|
||||||
menuItems.push({
|
menuItems.push(addEnvironmentItem);
|
||||||
label: 'Create Sub Environment',
|
|
||||||
leftSlot: <Icon icon="plus" />,
|
|
||||||
hidden: !isBaseEnvironment(environment),
|
|
||||||
onSelect: async () => {
|
|
||||||
await createSubEnvironment();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuItems;
|
return menuItems;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Environment } from '@yaakapp-internal/models';
|
|||||||
import { patchModel } from '@yaakapp-internal/models';
|
import { patchModel } from '@yaakapp-internal/models';
|
||||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||||
import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled';
|
import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||||
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import React from 'react';
|
|
||||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||||
import { appInfo } from '../../lib/appInfo';
|
import { appInfo } from '../../lib/appInfo';
|
||||||
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
|
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import {
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
import { selectAtom } from 'jotai/utils';
|
import { selectAtom } from 'jotai/utils';
|
||||||
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useKey } from 'react-use';
|
|
||||||
import { moveToWorkspace } from '../commands/moveToWorkspace';
|
import { moveToWorkspace } from '../commands/moveToWorkspace';
|
||||||
import { openFolderSettings } from '../commands/openFolderSettings';
|
import { openFolderSettings } from '../commands/openFolderSettings';
|
||||||
import { activeCookieJarAtom } from '../hooks/useActiveCookieJar';
|
import { activeCookieJarAtom } from '../hooks/useActiveCookieJar';
|
||||||
@@ -150,33 +149,31 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
items.map((m, i) =>
|
items.map((m, i) =>
|
||||||
// Spread item sortPriority out over before/after range
|
// 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) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTreeRefInit = useCallback((n: TreeHandle) => {
|
const handleTreeRefInit = useCallback(
|
||||||
treeRef.current = n;
|
(n: TreeHandle) => {
|
||||||
if (n == null) return;
|
treeRef.current = n;
|
||||||
const activeId = jotaiStore.get(activeIdAtom);
|
if (n == null) return;
|
||||||
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 activeId = jotaiStore.get(activeIdAtom);
|
const activeId = jotaiStore.get(activeIdAtom);
|
||||||
if (activeId == null) return;
|
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(() => {
|
const clearFilterText = useCallback(() => {
|
||||||
jotaiStore.set(sidebarFilterAtom, { text: '', key: `${Math.random()}` });
|
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 actions = useMemo(() => {
|
||||||
const enable = () => treeRef.current?.hasFocus() ?? false;
|
const enable = () => treeRef.current?.hasFocus() ?? false;
|
||||||
|
|
||||||
@@ -291,7 +280,11 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
|
|
||||||
// No children means we're in the root
|
// No children means we're in the root
|
||||||
if (child == null) {
|
if (child == null) {
|
||||||
return getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: null });
|
return getCreateDropdownItems({
|
||||||
|
workspaceId,
|
||||||
|
activeRequest: null,
|
||||||
|
folderId: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaces = jotaiStore.get(workspacesAtom);
|
const workspaces = jotaiStore.get(workspacesAtom);
|
||||||
@@ -355,12 +348,19 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
items.length === 1 && child.model === 'folder'
|
items.length === 1 && child.model === 'folder'
|
||||||
? [
|
? [
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
...getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: child.id }),
|
...getCreateDropdownItems({
|
||||||
|
workspaceId,
|
||||||
|
activeRequest: null,
|
||||||
|
folderId: child.id,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
const menuItems: ContextMenuProps['items'] = [
|
const menuItems: ContextMenuProps['items'] = [
|
||||||
...initialItems,
|
...initialItems,
|
||||||
{ type: 'separator', hidden: initialItems.filter((v) => !v.hidden).length === 0 },
|
{
|
||||||
|
type: 'separator',
|
||||||
|
hidden: initialItems.filter((v) => !v.hidden).length === 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Rename',
|
label: 'Rename',
|
||||||
leftSlot: <Icon icon="pencil" />,
|
leftSlot: <Icon icon="pencil" />,
|
||||||
@@ -421,7 +421,9 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
const view = filterRef.current;
|
const view = filterRef.current;
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
const ext = filter({ fields: allFields ?? [] });
|
const ext = filter({ fields: allFields ?? [] });
|
||||||
view.dispatch({ effects: filterLanguageCompartmentRef.current.reconfigure(ext) });
|
view.dispatch({
|
||||||
|
effects: filterLanguageCompartmentRef.current.reconfigure(ext),
|
||||||
|
});
|
||||||
}, [allFields]);
|
}, [allFields]);
|
||||||
|
|
||||||
if (tree == null || hidden) {
|
if (tree == null || hidden) {
|
||||||
@@ -434,7 +436,7 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
aria-hidden={hidden ?? undefined}
|
aria-hidden={hidden ?? undefined}
|
||||||
className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)_auto]')}
|
className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)_auto]')}
|
||||||
>
|
>
|
||||||
<div className="px-3 pt-3 grid grid-cols-[1fr_auto] items-center -mr-2.5">
|
<div className="w-full px-3 pt-3 grid grid-cols-[minmax(0,1fr)_auto] items-center -mr-2.5">
|
||||||
{(tree.children?.length ?? 0) > 0 && (
|
{(tree.children?.length ?? 0) > 0 && (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
@@ -551,7 +553,10 @@ const allPotentialChildrenAtom = atom<SidebarModel[]>((get) => {
|
|||||||
|
|
||||||
const memoAllPotentialChildrenAtom = deepEqualAtom(allPotentialChildrenAtom);
|
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<SidebarModel>, FieldDef[]] | null>((get) => {
|
const sidebarTreeAtom = atom<[TreeNode<SidebarModel>, FieldDef[]] | null>((get) => {
|
||||||
const allModels = get(memoAllPotentialChildrenAtom);
|
const allModels = get(memoAllPotentialChildrenAtom);
|
||||||
@@ -580,21 +585,24 @@ const sidebarTreeAtom = atom<[TreeNode<SidebarModel>, FieldDef[]] | null>((get)
|
|||||||
const build = (node: TreeNode<SidebarModel>, depth: number): boolean => {
|
const build = (node: TreeNode<SidebarModel>, depth: number): boolean => {
|
||||||
const childItems = childrenMap[node.item.id] ?? [];
|
const childItems = childrenMap[node.item.id] ?? [];
|
||||||
let matchesSelf = true;
|
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)) {
|
for (const [field, value] of Object.entries(fields)) {
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
allFields[field] = allFields[field] ?? new Set();
|
allFields[field] = allFields[field] ?? new Set();
|
||||||
allFields[field].add(value);
|
allFields[field].add(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryAst != null) {
|
if (queryAst != null) {
|
||||||
matchesSelf = evaluate(queryAst, { text: getItemText(node.item), fields });
|
matchesSelf = isLeafNode && evaluate(queryAst, { text: getItemText(node.item), fields });
|
||||||
}
|
}
|
||||||
|
|
||||||
let matchesChild = false;
|
let matchesChild = false;
|
||||||
|
|
||||||
// Recurse to children
|
// Recurse to children
|
||||||
const m = node.item.model;
|
node.children = !isLeafNode ? [] : undefined;
|
||||||
node.children = m === 'folder' || m === 'workspace' ? [] : undefined;
|
|
||||||
|
|
||||||
if (node.children != null) {
|
if (node.children != null) {
|
||||||
childItems.sort((a, b) => {
|
childItems.sort((a, b) => {
|
||||||
@@ -623,7 +631,7 @@ const sidebarTreeAtom = atom<[TreeNode<SidebarModel>, FieldDef[]] | null>((get)
|
|||||||
|
|
||||||
const root: TreeNode<SidebarModel> = {
|
const root: TreeNode<SidebarModel> = {
|
||||||
item: activeWorkspace,
|
item: activeWorkspace,
|
||||||
parent: null,
|
parent: null,
|
||||||
children: [],
|
children: [],
|
||||||
depth: 0,
|
depth: 0,
|
||||||
};
|
};
|
||||||
@@ -633,7 +641,10 @@ const sidebarTreeAtom = atom<[TreeNode<SidebarModel>, FieldDef[]] | null>((get)
|
|||||||
|
|
||||||
const fields: FieldDef[] = [];
|
const fields: FieldDef[] = [];
|
||||||
for (const [name, values] of Object.entries(allFields)) {
|
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;
|
return [root, fields] as const;
|
||||||
});
|
});
|
||||||
@@ -716,7 +727,9 @@ const SidebarInnerItem = memo(function SidebarInnerItem({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function getItemFields(item: SidebarModel): Record<string, string> {
|
function getItemFields(node: TreeNode<SidebarModel>): Record<string, string> {
|
||||||
|
const item = node.item;
|
||||||
|
|
||||||
if (item.model === 'workspace') return {};
|
if (item.model === 'workspace') return {};
|
||||||
|
|
||||||
const fields: Record<string, string> = {};
|
const fields: Record<string, string> = {};
|
||||||
@@ -736,9 +749,20 @@ function getItemFields(item: SidebarModel): Record<string, string> {
|
|||||||
if (item.model === 'grpc_request') fields.type = 'grpc';
|
if (item.model === 'grpc_request') fields.type = 'grpc';
|
||||||
else if (item.model === 'websocket_request') fields.type = 'ws';
|
else if (item.model === 'websocket_request') fields.type = 'ws';
|
||||||
|
|
||||||
|
if (node.parent?.item.model === 'folder') {
|
||||||
|
fields.folder = node.parent.item.name;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemText(item: SidebarModel): string {
|
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(' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,36 @@
|
|||||||
import {
|
import { enableEncryption, revealWorkspaceKey, setWorkspaceKey } from '@yaakapp-internal/crypto';
|
||||||
enableEncryption,
|
import type { WorkspaceMeta } from '@yaakapp-internal/models';
|
||||||
revealWorkspaceKey,
|
import classNames from 'classnames';
|
||||||
setWorkspaceKey,
|
import { useAtomValue } from 'jotai';
|
||||||
} from "@yaakapp-internal/crypto";
|
import { useEffect, useState } from 'react';
|
||||||
import type { WorkspaceMeta } from "@yaakapp-internal/models";
|
import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../hooks/useActiveWorkspace';
|
||||||
import classNames from "classnames";
|
import { createFastMutation } from '../hooks/useFastMutation';
|
||||||
import { useAtomValue } from "jotai";
|
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||||
import { useEffect, useState } from "react";
|
import { CopyIconButton } from './CopyIconButton';
|
||||||
import {
|
import { Banner } from './core/Banner';
|
||||||
activeWorkspaceAtom,
|
import type { ButtonProps } from './core/Button';
|
||||||
activeWorkspaceMetaAtom,
|
import { Button } from './core/Button';
|
||||||
} from "../hooks/useActiveWorkspace";
|
import { IconButton } from './core/IconButton';
|
||||||
import { createFastMutation } from "../hooks/useFastMutation";
|
import { IconTooltip } from './core/IconTooltip';
|
||||||
import { useStateWithDeps } from "../hooks/useStateWithDeps";
|
import { Label } from './core/Label';
|
||||||
import { CopyIconButton } from "./CopyIconButton";
|
import { PlainInput } from './core/PlainInput';
|
||||||
import { Banner } from "./core/Banner";
|
import { HStack, VStack } from './core/Stacks';
|
||||||
import type { ButtonProps } from "./core/Button";
|
import { EncryptionHelp } from './EncryptionHelp';
|
||||||
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 {
|
interface Props {
|
||||||
size?: ButtonProps["size"];
|
size?: ButtonProps['size'];
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
onDone?: () => void;
|
onDone?: () => void;
|
||||||
onEnabledEncryption?: () => void;
|
onEnabledEncryption?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceEncryptionSetting(
|
export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEncryption }: Props) {
|
||||||
{ size, expanded, onDone, onEnabledEncryption }: Props,
|
const [justEnabledEncryption, setJustEnabledEncryption] = useState<boolean>(false);
|
||||||
) {
|
|
||||||
const [justEnabledEncryption, setJustEnabledEncryption] = useState<boolean>(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||||
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
||||||
const [key, setKey] = useState<
|
const [key, setKey] = useState<{ key: string | null; error: string | null } | null>(null);
|
||||||
{ key: string | null; error: string | null } | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspaceMeta == null) {
|
if (workspaceMeta == null) {
|
||||||
@@ -122,7 +109,7 @@ export function WorkspaceEncryptionSetting(
|
|||||||
return (
|
return (
|
||||||
<div className="mb-auto flex flex-col-reverse">
|
<div className="mb-auto flex flex-col-reverse">
|
||||||
<Button
|
<Button
|
||||||
color={expanded ? "info" : "secondary"}
|
color={expanded ? 'info' : 'secondary'}
|
||||||
size={size}
|
size={size}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -130,30 +117,32 @@ export function WorkspaceEncryptionSetting(
|
|||||||
await enableEncryption(workspaceMeta.workspaceId);
|
await enableEncryption(workspaceMeta.workspaceId);
|
||||||
setJustEnabledEncryption(true);
|
setJustEnabledEncryption(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Failed to enable encryption: " + err);
|
setError('Failed to enable encryption: ' + err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Enable Encryption
|
Enable Encryption
|
||||||
</Button>
|
</Button>
|
||||||
{error && <Banner color="danger" className="mb-2">{error}</Banner>}
|
{error && (
|
||||||
{expanded
|
<Banner color="danger" className="mb-2">
|
||||||
? (
|
{error}
|
||||||
<Banner color="info" className="mb-6">
|
</Banner>
|
||||||
<EncryptionHelp />
|
)}
|
||||||
</Banner>
|
{expanded ? (
|
||||||
)
|
<Banner color="info" className="mb-6">
|
||||||
: (
|
<EncryptionHelp />
|
||||||
<Label htmlFor={null} help={<EncryptionHelp />}>
|
</Banner>
|
||||||
Workspace encryption
|
) : (
|
||||||
</Label>
|
<Label htmlFor={null} help={<EncryptionHelp />}>
|
||||||
)}
|
Workspace encryption
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const setWorkspaceKeyMut = createFastMutation({
|
const setWorkspaceKeyMut = createFastMutation({
|
||||||
mutationKey: ["set-workspace-key"],
|
mutationKey: ['set-workspace-key'],
|
||||||
mutationFn: setWorkspaceKey,
|
mutationFn: setWorkspaceKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,13 +155,15 @@ function EnterWorkspaceKey({
|
|||||||
onEnabled?: () => void;
|
onEnabled?: () => void;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const [key, setKey] = useState<string>("");
|
const [key, setKey] = useState<string>('');
|
||||||
return (
|
return (
|
||||||
<VStack space={4} className="w-full">
|
<VStack space={4} className="w-full">
|
||||||
{error ? <Banner color="danger">{error}</Banner> : (
|
{error ? (
|
||||||
|
<Banner color="danger">{error}</Banner>
|
||||||
|
) : (
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
This workspace contains encrypted values but no key is configured.
|
This workspace contains encrypted values but no key is configured. Please enter the
|
||||||
Please enter the workspace key to access the encrypted data.
|
workspace key to access the encrypted data.
|
||||||
</Banner>
|
</Banner>
|
||||||
)}
|
)}
|
||||||
<HStack
|
<HStack
|
||||||
@@ -219,35 +210,24 @@ function KeyRevealer({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full border border-border rounded-md pl-3 py-2 p-1",
|
'w-full border border-border rounded-md pl-3 py-2 p-1',
|
||||||
"grid gap-1 grid-cols-[minmax(0,1fr)_auto] items-center",
|
'grid gap-1 grid-cols-[minmax(0,1fr)_auto] items-center',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<VStack space={0.5}>
|
<VStack space={0.5}>
|
||||||
{!disableLabel && (
|
{!disableLabel && (
|
||||||
<span className="text-sm text-primary flex items-center gap-1">
|
<span className="text-sm text-primary flex items-center gap-1">
|
||||||
Workspace encryption key{" "}
|
Workspace encryption key{' '}
|
||||||
<IconTooltip
|
<IconTooltip iconSize="sm" size="lg" content={helpAfterEncryption} />
|
||||||
iconSize="sm"
|
|
||||||
size="lg"
|
|
||||||
content={helpAfterEncryption}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{encryptionKey && (
|
{encryptionKey && <HighlightedKey keyText={encryptionKey} show={show} />}
|
||||||
<HighlightedKey
|
|
||||||
keyText={encryptionKey}
|
|
||||||
show={show}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</VStack>
|
</VStack>
|
||||||
<HStack>
|
<HStack>
|
||||||
{encryptionKey && (
|
{encryptionKey && <CopyIconButton text={encryptionKey} title="Copy workspace key" />}
|
||||||
<CopyIconButton text={encryptionKey} title="Copy workspace key" />
|
|
||||||
)}
|
|
||||||
<IconButton
|
<IconButton
|
||||||
title={show ? "Hide" : "Reveal" + "workspace key"}
|
title={show ? 'Hide' : 'Reveal' + 'workspace key'}
|
||||||
icon={show ? "eye_closed" : "eye"}
|
icon={show ? 'eye_closed' : 'eye'}
|
||||||
onClick={() => setShow((v) => !v)}
|
onClick={() => setShow((v) => !v)}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -258,32 +238,31 @@ function KeyRevealer({
|
|||||||
function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) {
|
function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs font-mono [&_*]:cursor-auto [&_*]:select-text">
|
<span className="text-xs font-mono [&_*]:cursor-auto [&_*]:select-text">
|
||||||
{show
|
{show ? (
|
||||||
? (
|
keyText.split('').map((c, i) => {
|
||||||
keyText.split("").map((c, i) => {
|
return (
|
||||||
return (
|
<span
|
||||||
<span
|
key={i}
|
||||||
key={i}
|
className={classNames(
|
||||||
className={classNames(
|
c.match(/[0-9]/) && 'text-info',
|
||||||
c.match(/[0-9]/) && "text-info",
|
c == '-' && 'text-text-subtle',
|
||||||
c == "-" && "text-text-subtle",
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{c}
|
||||||
{c}
|
</span>
|
||||||
</span>
|
);
|
||||||
);
|
})
|
||||||
})
|
) : (
|
||||||
)
|
<div className="text-text-subtle">•••••••••••••••••••••</div>
|
||||||
: <div className="text-text-subtle">•••••••••••••••••••••</div>}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const helpAfterEncryption = (
|
const helpAfterEncryption = (
|
||||||
<p>
|
<p>
|
||||||
The following key is used for encryption operations within this workspace.
|
The following key is used for encryption operations within this workspace. It is stored securely
|
||||||
It is stored securely using your OS keychain, but it is recommended to back
|
using your OS keychain, but it is recommended to back it up. If you share this workspace with
|
||||||
it up. If you share this workspace with others, you'll need to send
|
others, you'll need to send them this key to access any encrypted values.
|
||||||
them this key to access any encrypted values.
|
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import type {
|
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
|
||||||
DragEndEvent,
|
|
||||||
DragMoveEvent,
|
|
||||||
DragStartEvent,
|
|
||||||
} from "@dnd-kit/core";
|
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
MeasuringStrategy,
|
MeasuringStrategy,
|
||||||
@@ -11,16 +7,10 @@ import {
|
|||||||
useDroppable,
|
useDroppable,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from "@dnd-kit/core";
|
} from '@dnd-kit/core';
|
||||||
import { type } from "@tauri-apps/plugin-os";
|
import { type } from '@tauri-apps/plugin-os';
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames';
|
||||||
import type {
|
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react';
|
||||||
ComponentType,
|
|
||||||
MouseEvent,
|
|
||||||
ReactElement,
|
|
||||||
Ref,
|
|
||||||
RefAttributes,
|
|
||||||
} from "react";
|
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
memo,
|
||||||
@@ -30,14 +20,14 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} 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';
|
||||||
import { computeSideForDragMove } from "../../../lib/dnd";
|
import { computeSideForDragMove } from '../../../lib/dnd';
|
||||||
import { jotaiStore } from "../../../lib/jotai";
|
import { jotaiStore } from '../../../lib/jotai';
|
||||||
import type { ContextMenuProps, DropdownItem } from "../Dropdown";
|
import type { ContextMenuProps, DropdownItem } from '../Dropdown';
|
||||||
import { ContextMenu } from "../Dropdown";
|
import { ContextMenu } from '../Dropdown';
|
||||||
import {
|
import {
|
||||||
collapsedFamily,
|
collapsedFamily,
|
||||||
draggingIdsFamily,
|
draggingIdsFamily,
|
||||||
@@ -45,23 +35,14 @@ import {
|
|||||||
hoveredParentFamily,
|
hoveredParentFamily,
|
||||||
isCollapsedFamily,
|
isCollapsedFamily,
|
||||||
selectedIdsFamily,
|
selectedIdsFamily,
|
||||||
} from "./atoms";
|
} from './atoms';
|
||||||
import type { SelectableTreeNode, TreeNode } from "./common";
|
import type { SelectableTreeNode, TreeNode } from './common';
|
||||||
import {
|
import { closestVisibleNode, equalSubtree, getSelectedItems, hasAncestor } from './common';
|
||||||
closestVisibleNode,
|
import { TreeDragOverlay } from './TreeDragOverlay';
|
||||||
equalSubtree,
|
import type { TreeItemClickEvent, TreeItemHandle, TreeItemProps } from './TreeItem';
|
||||||
getSelectedItems,
|
import type { TreeItemListProps } from './TreeItemList';
|
||||||
hasAncestor,
|
import { TreeItemList } from './TreeItemList';
|
||||||
} from "./common";
|
import { useSelectableItems } from './useSelectableItems';
|
||||||
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 */
|
/** So we re-calculate after expanding a folder during drag */
|
||||||
const measuring = { droppable: { strategy: MeasuringStrategy.Always } };
|
const measuring = { droppable: { strategy: MeasuringStrategy.Always } };
|
||||||
@@ -70,21 +51,15 @@ export interface TreeProps<T extends { id: string }> {
|
|||||||
root: TreeNode<T>;
|
root: TreeNode<T>;
|
||||||
treeId: string;
|
treeId: string;
|
||||||
getItemKey: (item: T) => string;
|
getItemKey: (item: T) => string;
|
||||||
getContextMenu?: (
|
getContextMenu?: (items: T[]) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
|
||||||
items: T[],
|
|
||||||
) => ContextMenuProps["items"] | Promise<ContextMenuProps["items"]>;
|
|
||||||
ItemInner: ComponentType<{ treeId: string; item: T }>;
|
ItemInner: ComponentType<{ treeId: string; item: T }>;
|
||||||
ItemLeftSlotInner?: ComponentType<{ treeId: string; item: T }>;
|
ItemLeftSlotInner?: ComponentType<{ treeId: string; item: T }>;
|
||||||
ItemRightSlot?: ComponentType<{ treeId: string; item: T }>;
|
ItemRightSlot?: ComponentType<{ treeId: string; item: T }>;
|
||||||
className?: string;
|
className?: string;
|
||||||
onActivate?: (item: T) => void;
|
onActivate?: (item: T) => void;
|
||||||
onDragEnd?: (
|
onDragEnd?: (opt: { items: T[]; parent: T; children: T[]; insertAt: number }) => void;
|
||||||
opt: { items: T[]; parent: T; children: T[]; insertAt: number },
|
|
||||||
) => void;
|
|
||||||
hotkeys?: {
|
hotkeys?: {
|
||||||
actions: Partial<
|
actions: Partial<Record<HotkeyAction, { cb: (items: T[]) => void } & HotKeyOptions>>;
|
||||||
Record<HotkeyAction, { cb: (items: T[]) => void } & HotKeyOptions>
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
getEditOptions?: (item: T) => {
|
getEditOptions?: (item: T) => {
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
@@ -121,24 +96,19 @@ function TreeInner<T extends { id: string }>(
|
|||||||
) {
|
) {
|
||||||
const treeRef = useRef<HTMLDivElement>(null);
|
const treeRef = useRef<HTMLDivElement>(null);
|
||||||
const selectableItems = useSelectableItems(root);
|
const selectableItems = useSelectableItems(root);
|
||||||
const [showContextMenu, setShowContextMenu] = useState<
|
const [showContextMenu, setShowContextMenu] = useState<{
|
||||||
{
|
items: DropdownItem[];
|
||||||
items: DropdownItem[];
|
x: number;
|
||||||
x: number;
|
y: number;
|
||||||
y: number;
|
} | null>(null);
|
||||||
} | null
|
|
||||||
>(null);
|
|
||||||
const treeItemRefs = useRef<Record<string, TreeItemHandle>>({});
|
const treeItemRefs = useRef<Record<string, TreeItemHandle>>({});
|
||||||
const handleAddTreeItemRef = useCallback(
|
const handleAddTreeItemRef = useCallback((item: T, r: TreeItemHandle | null) => {
|
||||||
(item: T, r: TreeItemHandle | null) => {
|
if (r == null) {
|
||||||
if (r == null) {
|
delete treeItemRefs.current[item.id];
|
||||||
delete treeItemRefs.current[item.id];
|
} else {
|
||||||
} else {
|
treeItemRefs.current[item.id] = r;
|
||||||
treeItemRefs.current[item.id] = r;
|
}
|
||||||
}
|
}, []);
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Select the first item on first render
|
// Select the first item on first render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -176,8 +146,8 @@ function TreeInner<T extends { id: string }>(
|
|||||||
|
|
||||||
const ensureTabbableItem = useCallback(() => {
|
const ensureTabbableItem = useCallback(() => {
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const lastSelectedItem = selectableItems.find((i) =>
|
const lastSelectedItem = selectableItems.find(
|
||||||
i.node.item.id === lastSelectedId && !i.node.hidden
|
(i) => i.node.item.id === lastSelectedId && !i.node.hidden,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If no item found, default to selecting the first item (prefer leaf node);
|
// If no item found, default to selecting the first item (prefer leaf node);
|
||||||
@@ -224,8 +194,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
() => ({
|
() => ({
|
||||||
treeId,
|
treeId,
|
||||||
focus: tryFocus,
|
focus: tryFocus,
|
||||||
hasFocus: () =>
|
hasFocus: () => treeRef.current?.contains(document.activeElement) ?? false,
|
||||||
treeRef.current?.contains(document.activeElement) ?? false,
|
|
||||||
renameItem: (id) => treeItemRefs.current[id]?.rename(),
|
renameItem: (id) => treeItemRefs.current[id]?.rename(),
|
||||||
selectItem: (id) => {
|
selectItem: (id) => {
|
||||||
if (jotaiStore.get(selectedIdsFamily(treeId)).includes(id)) {
|
if (jotaiStore.get(selectedIdsFamily(treeId)).includes(id)) {
|
||||||
@@ -240,9 +209,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const items = getSelectedItems(treeId, selectableItems);
|
const items = getSelectedItems(treeId, selectableItems);
|
||||||
const menuItems = await getContextMenu(items);
|
const menuItems = await getContextMenu(items);
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const rect = lastSelectedId
|
const rect = lastSelectedId ? treeItemRefs.current[lastSelectedId]?.rect() : null;
|
||||||
? treeItemRefs.current[lastSelectedId]?.rect()
|
|
||||||
: null;
|
|
||||||
if (rect == null) return;
|
if (rect == null) return;
|
||||||
setShowContextMenu({ items: menuItems, x: rect.x, y: rect.y });
|
setShowContextMenu({ items: menuItems, x: rect.x, y: rect.y });
|
||||||
},
|
},
|
||||||
@@ -264,66 +231,43 @@ function TreeInner<T extends { id: string }>(
|
|||||||
// If right-clicked an item that was NOT in the multiple-selection, just use that one
|
// If right-clicked an item that was NOT in the multiple-selection, just use that one
|
||||||
// Also update the selection with it
|
// Also update the selection with it
|
||||||
setSelected([item.id], false);
|
setSelected([item.id], false);
|
||||||
jotaiStore.set(
|
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
|
||||||
focusIdsFamily(treeId),
|
|
||||||
(prev) => ({ ...prev, lastId: item.id }),
|
|
||||||
);
|
|
||||||
return getContextMenu([item]);
|
return getContextMenu([item]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [getContextMenu, selectableItems, setSelected, treeId]);
|
}, [getContextMenu, selectableItems, setSelected, treeId]);
|
||||||
|
|
||||||
const handleSelect = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
|
const handleSelect = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
|
||||||
(item, { shiftKey, metaKey, ctrlKey }) => {
|
(item, { shiftKey, metaKey, ctrlKey }) => {
|
||||||
const anchorSelectedId = jotaiStore.get(focusIdsFamily(treeId)).anchorId;
|
const anchorSelectedId = jotaiStore.get(focusIdsFamily(treeId)).anchorId;
|
||||||
const selectedIdsAtom = selectedIdsFamily(treeId);
|
const selectedIdsAtom = selectedIdsFamily(treeId);
|
||||||
const selectedIds = jotaiStore.get(selectedIdsAtom);
|
const selectedIds = jotaiStore.get(selectedIdsAtom);
|
||||||
|
|
||||||
// Mark the item as the last one selected
|
// Mark the item as the last one selected
|
||||||
jotaiStore.set(
|
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
|
||||||
focusIdsFamily(treeId),
|
|
||||||
(prev) => ({ ...prev, lastId: item.id }),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shiftKey) {
|
if (shiftKey) {
|
||||||
const anchorIndex = selectableItems.findIndex((i) =>
|
const validSelectableItems = getValidSelectableItems(treeId, selectableItems);
|
||||||
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 = selectableItems.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
|
||||||
if (
|
if (selectedIds.length === 0 || anchorIndex === -1 || currIndex === -1) {
|
||||||
selectedIds.length === 0 || anchorIndex === -1 || currIndex === -1
|
|
||||||
) {
|
|
||||||
setSelected([item.id], true);
|
setSelected([item.id], true);
|
||||||
jotaiStore.set(
|
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, anchorId: item.id }));
|
||||||
focusIdsFamily(treeId),
|
|
||||||
(prev) => ({ ...prev, anchorId: item.id }),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validSelectableItems = getValidSelectableItems(
|
|
||||||
treeId,
|
|
||||||
selectableItems,
|
|
||||||
);
|
|
||||||
if (currIndex > anchorIndex) {
|
if (currIndex > anchorIndex) {
|
||||||
// Selecting down
|
// Selecting down
|
||||||
const itemsToSelect = validSelectableItems.slice(
|
const itemsToSelect = validSelectableItems.slice(anchorIndex, currIndex + 1);
|
||||||
anchorIndex,
|
|
||||||
currIndex + 1,
|
|
||||||
);
|
|
||||||
setSelected(
|
setSelected(
|
||||||
itemsToSelect.map((v) => v.node.item.id),
|
itemsToSelect.map((v) => v.node.item.id),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
} else if (currIndex < anchorIndex) {
|
} else if (currIndex < anchorIndex) {
|
||||||
// Selecting up
|
// Selecting up
|
||||||
const itemsToSelect = validSelectableItems.slice(
|
const itemsToSelect = validSelectableItems.slice(currIndex, anchorIndex + 1);
|
||||||
currIndex,
|
|
||||||
anchorIndex + 1,
|
|
||||||
);
|
|
||||||
setSelected(
|
setSelected(
|
||||||
itemsToSelect.map((v) => v.node.item.id),
|
itemsToSelect.map((v) => v.node.item.id),
|
||||||
true,
|
true,
|
||||||
@@ -331,7 +275,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
} else {
|
} else {
|
||||||
setSelected([item.id], true);
|
setSelected([item.id], true);
|
||||||
}
|
}
|
||||||
} else if (type() === "macos" ? metaKey : ctrlKey) {
|
} else if (type() === 'macos' ? metaKey : ctrlKey) {
|
||||||
const withoutCurr = selectedIds.filter((id) => id !== item.id);
|
const withoutCurr = selectedIds.filter((id) => id !== item.id);
|
||||||
if (withoutCurr.length === selectedIds.length) {
|
if (withoutCurr.length === selectedIds.length) {
|
||||||
// It wasn't in there, so add it
|
// It wasn't in there, so add it
|
||||||
@@ -343,16 +287,13 @@ function TreeInner<T extends { id: string }>(
|
|||||||
} else {
|
} else {
|
||||||
// Select single
|
// Select single
|
||||||
setSelected([item.id], true);
|
setSelected([item.id], true);
|
||||||
jotaiStore.set(
|
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, anchorId: item.id }));
|
||||||
focusIdsFamily(treeId),
|
|
||||||
(prev) => ({ ...prev, anchorId: item.id }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selectableItems, setSelected, treeId],
|
[selectableItems, setSelected, treeId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
|
const handleClick = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
|
||||||
(item, e) => {
|
(item, e) => {
|
||||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||||
handleSelect(item, e);
|
handleSelect(item, e);
|
||||||
@@ -367,13 +308,8 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const selectPrevItem = useCallback(
|
const selectPrevItem = useCallback(
|
||||||
(e: TreeItemClickEvent) => {
|
(e: TreeItemClickEvent) => {
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const validSelectableItems = getValidSelectableItems(
|
const validSelectableItems = getValidSelectableItems(treeId, selectableItems);
|
||||||
treeId,
|
const index = validSelectableItems.findIndex((i) => i.node.item.id === lastSelectedId);
|
||||||
selectableItems,
|
|
||||||
);
|
|
||||||
const index = validSelectableItems.findIndex((i) =>
|
|
||||||
i.node.item.id === lastSelectedId
|
|
||||||
);
|
|
||||||
const item = validSelectableItems[index - 1];
|
const item = validSelectableItems[index - 1];
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
handleSelect(item.node.item, e);
|
handleSelect(item.node.item, e);
|
||||||
@@ -385,13 +321,8 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const selectNextItem = useCallback(
|
const selectNextItem = useCallback(
|
||||||
(e: TreeItemClickEvent) => {
|
(e: TreeItemClickEvent) => {
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const validSelectableItems = getValidSelectableItems(
|
const validSelectableItems = getValidSelectableItems(treeId, selectableItems);
|
||||||
treeId,
|
const index = validSelectableItems.findIndex((i) => i.node.item.id === lastSelectedId);
|
||||||
selectableItems,
|
|
||||||
);
|
|
||||||
const index = validSelectableItems.findIndex((i) =>
|
|
||||||
i.node.item.id === lastSelectedId
|
|
||||||
);
|
|
||||||
const item = validSelectableItems[index + 1];
|
const item = validSelectableItems[index + 1];
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
handleSelect(item.node.item, e);
|
handleSelect(item.node.item, e);
|
||||||
@@ -404,8 +335,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
(e: TreeItemClickEvent) => {
|
(e: TreeItemClickEvent) => {
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const lastSelectedItem =
|
const lastSelectedItem =
|
||||||
selectableItems.find((i) => i.node.item.id === lastSelectedId)?.node ??
|
selectableItems.find((i) => i.node.item.id === lastSelectedId)?.node ?? null;
|
||||||
null;
|
|
||||||
if (lastSelectedItem?.parent != null) {
|
if (lastSelectedItem?.parent != null) {
|
||||||
handleSelect(lastSelectedItem.parent.item, e);
|
handleSelect(lastSelectedItem.parent.item, e);
|
||||||
}
|
}
|
||||||
@@ -414,7 +344,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
useKey(
|
useKey(
|
||||||
(e) => e.key === "ArrowUp" || e.key.toLowerCase() === "k",
|
(e) => e.key === 'ArrowUp' || e.key.toLowerCase() === 'k',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!isTreeFocused()) return;
|
if (!isTreeFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -425,7 +355,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
useKey(
|
useKey(
|
||||||
(e) => e.key === "ArrowDown" || e.key.toLowerCase() === "j",
|
(e) => e.key === 'ArrowDown' || e.key.toLowerCase() === 'j',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!isTreeFocused()) return;
|
if (!isTreeFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -437,26 +367,21 @@ function TreeInner<T extends { id: string }>(
|
|||||||
|
|
||||||
// If the selected item is a collapsed folder, expand it. Otherwise, select next item
|
// If the selected item is a collapsed folder, expand it. Otherwise, select next item
|
||||||
useKey(
|
useKey(
|
||||||
(e) => e.key === "ArrowRight" || e.key === "l",
|
(e) => e.key === 'ArrowRight' || e.key === 'l',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!isTreeFocused()) return;
|
if (!isTreeFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const collapsed = jotaiStore.get(collapsedFamily(treeId));
|
const collapsed = jotaiStore.get(collapsedFamily(treeId));
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const lastSelectedItem = selectableItems.find((i) =>
|
const lastSelectedItem = selectableItems.find((i) => i.node.item.id === lastSelectedId);
|
||||||
i.node.item.id === lastSelectedId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lastSelectedId &&
|
lastSelectedId &&
|
||||||
lastSelectedItem?.node.children != null &&
|
lastSelectedItem?.node.children != null &&
|
||||||
collapsed[lastSelectedItem.node.item.id] === true
|
collapsed[lastSelectedItem.node.item.id] === true
|
||||||
) {
|
) {
|
||||||
jotaiStore.set(
|
jotaiStore.set(isCollapsedFamily({ treeId, itemId: lastSelectedId }), false);
|
||||||
isCollapsedFamily({ treeId, itemId: lastSelectedId }),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
selectNextItem(e);
|
selectNextItem(e);
|
||||||
}
|
}
|
||||||
@@ -468,26 +393,21 @@ function TreeInner<T extends { id: string }>(
|
|||||||
// If the selected item is in a folder, select its parent.
|
// If the selected item is in a folder, select its parent.
|
||||||
// If the selected item is an expanded folder, collapse it.
|
// If the selected item is an expanded folder, collapse it.
|
||||||
useKey(
|
useKey(
|
||||||
(e) => e.key === "ArrowLeft" || e.key === "h",
|
(e) => e.key === 'ArrowLeft' || e.key === 'h',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!isTreeFocused()) return;
|
if (!isTreeFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const collapsed = jotaiStore.get(collapsedFamily(treeId));
|
const collapsed = jotaiStore.get(collapsedFamily(treeId));
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const lastSelectedItem = selectableItems.find((i) =>
|
const lastSelectedItem = selectableItems.find((i) => i.node.item.id === lastSelectedId);
|
||||||
i.node.item.id === lastSelectedId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lastSelectedId &&
|
lastSelectedId &&
|
||||||
lastSelectedItem?.node.children != null &&
|
lastSelectedItem?.node.children != null &&
|
||||||
collapsed[lastSelectedItem.node.item.id] !== true
|
collapsed[lastSelectedItem.node.item.id] !== true
|
||||||
) {
|
) {
|
||||||
jotaiStore.set(
|
jotaiStore.set(isCollapsedFamily({ treeId, itemId: lastSelectedId }), true);
|
||||||
isCollapsedFamily({ treeId, itemId: lastSelectedId }),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
selectParentItem(e);
|
selectParentItem(e);
|
||||||
}
|
}
|
||||||
@@ -496,7 +416,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
[selectableItems, handleSelect],
|
[selectableItems, handleSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeyPressEvent("Escape", async () => {
|
useKeyPressEvent('Escape', async () => {
|
||||||
if (!treeRef.current?.contains(document.activeElement)) return;
|
if (!treeRef.current?.contains(document.activeElement)) return;
|
||||||
clearDragState();
|
clearDragState();
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
@@ -535,22 +455,19 @@ function TreeInner<T extends { id: string }>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overSelectableItem =
|
const overSelectableItem = selectableItems.find((i) => i.node.item.id === over.id) ?? null;
|
||||||
selectableItems.find((i) => i.node.item.id === over.id) ?? null;
|
|
||||||
if (overSelectableItem == null) {
|
if (overSelectableItem == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const draggingItems = jotaiStore.get(draggingIdsFamily(treeId));
|
const draggingItems = jotaiStore.get(draggingIdsFamily(treeId));
|
||||||
for (const id of draggingItems) {
|
for (const id of draggingItems) {
|
||||||
const item = selectableItems.find((i) => i.node.item.id === id)?.node ??
|
const item = selectableItems.find((i) => i.node.item.id === id)?.node ?? null;
|
||||||
null;
|
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSameParent =
|
const isSameParent = item.parent?.item.id === overSelectableItem.node.parent?.item.id;
|
||||||
item.parent?.item.id === overSelectableItem.node.parent?.item.id;
|
|
||||||
if (item.localDrag && !isSameParent) {
|
if (item.localDrag && !isSameParent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -561,15 +478,13 @@ function TreeInner<T extends { id: string }>(
|
|||||||
|
|
||||||
const item = node.item;
|
const item = node.item;
|
||||||
let hoveredParent = node.parent;
|
let hoveredParent = node.parent;
|
||||||
const dragIndex =
|
const dragIndex = selectableItems.findIndex((n) => n.node.item.id === item.id) ?? -1;
|
||||||
selectableItems.findIndex((n) => n.node.item.id === item.id) ?? -1;
|
|
||||||
const hovered = selectableItems[dragIndex]?.node ?? null;
|
const hovered = selectableItems[dragIndex]?.node ?? null;
|
||||||
const hoveredIndex = dragIndex + (side === "above" ? 0 : 1);
|
const hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
||||||
let hoveredChildIndex = overSelectableItem.index +
|
let hoveredChildIndex = overSelectableItem.index + (side === 'above' ? 0 : 1);
|
||||||
(side === "above" ? 0 : 1);
|
|
||||||
|
|
||||||
// Move into the folder if it's open and we're moving below it
|
// 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;
|
hoveredParent = hovered;
|
||||||
hoveredChildIndex = 0;
|
hoveredChildIndex = 0;
|
||||||
}
|
}
|
||||||
@@ -601,9 +516,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
function handleDragStart(e: DragStartEvent) {
|
function handleDragStart(e: DragStartEvent) {
|
||||||
const selectedItems = getSelectedItems(treeId, selectableItems);
|
const selectedItems = getSelectedItems(treeId, selectableItems);
|
||||||
const isDraggingSelectedItem = selectedItems.find((i) =>
|
const isDraggingSelectedItem = selectedItems.find((i) => i.id === e.active.id);
|
||||||
i.id === e.active.id
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we started dragging an already-selected item, we'll use that
|
// If we started dragging an already-selected item, we'll use that
|
||||||
if (isDraggingSelectedItem) {
|
if (isDraggingSelectedItem) {
|
||||||
@@ -613,9 +526,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// If we started dragging a non-selected item, only drag that item
|
// If we started dragging a non-selected item, only drag that item
|
||||||
const activeItem = selectableItems.find((i) =>
|
const activeItem = selectableItems.find((i) => i.node.item.id === e.active.id)?.node.item;
|
||||||
i.node.item.id === e.active.id
|
|
||||||
)?.node.item;
|
|
||||||
if (activeItem != null) {
|
if (activeItem != null) {
|
||||||
jotaiStore.set(draggingIdsFamily(treeId), [activeItem.id]);
|
jotaiStore.set(draggingIdsFamily(treeId), [activeItem.id]);
|
||||||
// Also update selection to just be this one
|
// Also update selection to just be this one
|
||||||
@@ -656,30 +567,25 @@ function TreeInner<T extends { id: string }>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoveredParentS = hoveredParentId === root.item.id
|
const hoveredParentS =
|
||||||
? { node: root, depth: 0, index: 0 }
|
hoveredParentId === root.item.id
|
||||||
: (selectableItems.find((i) => i.node.item.id === hoveredParentId) ??
|
? { node: root, depth: 0, index: 0 }
|
||||||
null);
|
: (selectableItems.find((i) => i.node.item.id === hoveredParentId) ?? null);
|
||||||
const hoveredParent = hoveredParentS?.node ?? null;
|
const hoveredParent = hoveredParentS?.node ?? null;
|
||||||
|
|
||||||
if (
|
if (hoveredParent == null || hoveredIndex == null || !draggingItems?.length) {
|
||||||
hoveredParent == null || hoveredIndex == null || !draggingItems?.length
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the actual tree nodes for each dragged item (keeps order of draggingItems)
|
// Resolve the actual tree nodes for each dragged item (keeps order of draggingItems)
|
||||||
const draggedNodes: TreeNode<T>[] = draggingItems
|
const draggedNodes: TreeNode<T>[] = draggingItems
|
||||||
.map((id) => {
|
.map((id) => {
|
||||||
return selectableItems.find((i) => i.node.item.id === id)?.node ??
|
return selectableItems.find((i) => i.node.item.id === id)?.node ?? null;
|
||||||
null;
|
|
||||||
})
|
})
|
||||||
.filter((n) => n != null)
|
.filter((n) => n != null)
|
||||||
// Filter out invalid drags (dragging into descendant)
|
// Filter out invalid drags (dragging into descendant)
|
||||||
.filter(
|
.filter(
|
||||||
(n) =>
|
(n) => hoveredParent.item.id !== n.item.id && !hasAncestor(hoveredParent, n.item.id),
|
||||||
hoveredParent.item.id !== n.item.id &&
|
|
||||||
!hasAncestor(hoveredParent, n.item.id),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Work on a local copy of target children
|
// Work on a local copy of target children
|
||||||
@@ -708,7 +614,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
|
|
||||||
const treeItemListProps: Omit<
|
const treeItemListProps: Omit<
|
||||||
TreeItemListProps<T>,
|
TreeItemListProps<T>,
|
||||||
"nodes" | "treeId" | "activeIdAtom" | "hoveredParent" | "hoveredIndex"
|
'nodes' | 'treeId' | 'activeIdAtom' | 'hoveredParent' | 'hoveredIndex'
|
||||||
> = {
|
> = {
|
||||||
getItemKey,
|
getItemKey,
|
||||||
getContextMenu: handleGetContextMenu,
|
getContextMenu: handleGetContextMenu,
|
||||||
@@ -731,17 +637,11 @@ function TreeInner<T extends { id: string }>(
|
|||||||
[getContextMenu],
|
[getContextMenu],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TreeHotKeys
|
<TreeHotKeys treeId={treeId} hotkeys={hotkeys} selectableItems={selectableItems} />
|
||||||
treeId={treeId}
|
|
||||||
hotkeys={hotkeys}
|
|
||||||
selectableItems={selectableItems}
|
|
||||||
/>
|
|
||||||
{showContextMenu && (
|
{showContextMenu && (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
items={showContextMenu.items}
|
items={showContextMenu.items}
|
||||||
@@ -764,23 +664,23 @@ function TreeInner<T extends { id: string }>(
|
|||||||
ref={treeRef}
|
ref={treeRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"outline-none h-full",
|
'outline-none h-full',
|
||||||
"overflow-y-auto overflow-x-hidden",
|
'overflow-y-auto overflow-x-hidden',
|
||||||
"grid grid-rows-[auto_1fr]",
|
'grid grid-rows-[auto_1fr]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"[&_.tree-item.selected_.tree-item-inner]:text-text",
|
'[&_.tree-item.selected_.tree-item-inner]:text-text',
|
||||||
"[&:focus-within]:[&_.tree-item.selected]:bg-surface-active",
|
'[&:focus-within]:[&_.tree-item.selected]:bg-surface-active',
|
||||||
"[&:not(:focus-within)]:[&_.tree-item.selected]:bg-surface-highlight",
|
'[&:not(:focus-within)]:[&_.tree-item.selected]:bg-surface-highlight',
|
||||||
// Round the items, but only if the ends of the selection.
|
// Round the items, but only if the ends of the selection.
|
||||||
// Also account for the drop marker being in between items
|
// Also account for the drop marker being in between items
|
||||||
"[&_.tree-item]:rounded-md",
|
'[&_.tree-item]:rounded-md',
|
||||||
"[&_.tree-item.selected+.tree-item.selected]:rounded-t-none",
|
'[&_.tree-item.selected+.tree-item.selected]:rounded-t-none',
|
||||||
"[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none",
|
'[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none',
|
||||||
"[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none",
|
'[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none',
|
||||||
"[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none",
|
'[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TreeItemList
|
<TreeItemList
|
||||||
@@ -791,10 +691,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Assign root ID so we can reuse our same move/end logic */}
|
{/* Assign root ID so we can reuse our same move/end logic */}
|
||||||
<DropRegionAfterList
|
<DropRegionAfterList id={root.item.id} onContextMenu={handleContextMenu} />
|
||||||
id={root.item.id}
|
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<TreeDragOverlay
|
<TreeDragOverlay
|
||||||
treeId={treeId}
|
treeId={treeId}
|
||||||
@@ -816,10 +713,7 @@ export const Tree = memo(
|
|||||||
Tree_,
|
Tree_,
|
||||||
({ root: prevNode, ...prevProps }, { root: nextNode, ...nextProps }) => {
|
({ root: prevNode, ...prevProps }, { root: nextNode, ...nextProps }) => {
|
||||||
for (const key of Object.keys(prevProps)) {
|
for (const key of Object.keys(prevProps)) {
|
||||||
if (
|
if (prevProps[key as keyof typeof prevProps] !== nextProps[key as keyof typeof nextProps]) {
|
||||||
prevProps[key as keyof typeof prevProps] !==
|
|
||||||
nextProps[key as keyof typeof nextProps]
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -864,7 +758,7 @@ function TreeHotKey<T extends { id: string }>({
|
|||||||
...options,
|
...options,
|
||||||
enable: () => {
|
enable: () => {
|
||||||
if (enable == null) return true;
|
if (enable == null) return true;
|
||||||
if (typeof enable === "function") return enable();
|
if (typeof enable === 'function') return enable();
|
||||||
else return enable;
|
else return enable;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -878,7 +772,7 @@ function TreeHotKeys<T extends { id: string }>({
|
|||||||
selectableItems,
|
selectableItems,
|
||||||
}: {
|
}: {
|
||||||
treeId: string;
|
treeId: string;
|
||||||
hotkeys: TreeProps<T>["hotkeys"];
|
hotkeys: TreeProps<T>['hotkeys'];
|
||||||
selectableItems: SelectableTreeNode<T>[];
|
selectableItems: SelectableTreeNode<T>[];
|
||||||
}) {
|
}) {
|
||||||
if (hotkeys == null) return null;
|
if (hotkeys == null) return null;
|
||||||
|
|||||||
@@ -4,7 +4,18 @@ import { atom, useAtomValue } from 'jotai';
|
|||||||
export const environmentsBreakdownAtom = atom((get) => {
|
export const environmentsBreakdownAtom = atom((get) => {
|
||||||
const allEnvironments = get(environmentsAtom);
|
const allEnvironments = get(environmentsAtom);
|
||||||
const baseEnvironments = allEnvironments.filter((e) => e.parentModel === 'workspace') ?? [];
|
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 =
|
const folderEnvironments =
|
||||||
allEnvironments.filter((e) => e.parentModel === 'folder' && e.parentId != null) ?? [];
|
allEnvironments.filter((e) => e.parentModel === 'folder' && e.parentId != null) ?? [];
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useEffect, useMemo } from 'react';
|
|||||||
import { jotaiStore } from '../lib/jotai';
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
|
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
|
||||||
import { activeCookieJarAtom } from './useActiveCookieJar';
|
import { activeCookieJarAtom } from './useActiveCookieJar';
|
||||||
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
|
|
||||||
const kvKey = (workspaceId: string) => 'recent_cookie_jars::' + workspaceId;
|
const kvKey = (workspaceId: string) => 'recent_cookie_jars::' + workspaceId;
|
||||||
@@ -13,9 +12,8 @@ const fallback: string[] = [];
|
|||||||
|
|
||||||
export function useRecentCookieJars() {
|
export function useRecentCookieJars() {
|
||||||
const cookieJars = useAtomValue(cookieJarsAtom);
|
const cookieJars = useAtomValue(cookieJarsAtom);
|
||||||
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom);
|
|
||||||
const kv = useKeyValue<string[]>({
|
const kv = useKeyValue<string[]>({
|
||||||
key: kvKey(activeWorkspaceId ?? 'n/a'),
|
key: kvKey(cookieJars[0]?.workspaceId ?? 'n/a'),
|
||||||
namespace,
|
namespace,
|
||||||
fallback,
|
fallback,
|
||||||
});
|
});
|
||||||
@@ -31,18 +29,16 @@ export function useRecentCookieJars() {
|
|||||||
export function useSubscribeRecentCookieJars() {
|
export function useSubscribeRecentCookieJars() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return jotaiStore.sub(activeCookieJarAtom, async () => {
|
return jotaiStore.sub(activeCookieJarAtom, async () => {
|
||||||
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
const activeCookieJar = jotaiStore.get(activeCookieJarAtom);
|
||||||
const activeCookieJarId = jotaiStore.get(activeCookieJarAtom)?.id ?? null;
|
if (activeCookieJar == null) return;
|
||||||
if (activeWorkspaceId == null) return;
|
|
||||||
if (activeCookieJarId == null) return;
|
|
||||||
|
|
||||||
const key = kvKey(activeWorkspaceId);
|
const key = kvKey(activeCookieJar.workspaceId);
|
||||||
|
|
||||||
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
|
const recentIds = getKeyValue<string[]>({ 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 withoutActiveId = recentIds.filter((id) => id !== activeCookieJar.id);
|
||||||
const value = [activeCookieJarId, ...withoutActiveId];
|
const value = [activeCookieJar.id, ...withoutActiveId];
|
||||||
await setKeyValue({ namespace, key, value });
|
await setKeyValue({ namespace, key, value });
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { jotaiStore } from '../lib/jotai';
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
|
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
|
||||||
import { activeEnvironmentIdAtom } from './useActiveEnvironment';
|
import { activeEnvironmentAtom } from './useActiveEnvironment';
|
||||||
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from './useActiveWorkspace';
|
|
||||||
import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown';
|
import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown';
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
|
|
||||||
@@ -12,10 +10,9 @@ const namespace = 'global';
|
|||||||
const fallback: string[] = [];
|
const fallback: string[] = [];
|
||||||
|
|
||||||
export function useRecentEnvironments() {
|
export function useRecentEnvironments() {
|
||||||
const { subEnvironments } = useEnvironmentsBreakdown();
|
const { subEnvironments, allEnvironments } = useEnvironmentsBreakdown();
|
||||||
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
|
|
||||||
const kv = useKeyValue<string[]>({
|
const kv = useKeyValue<string[]>({
|
||||||
key: kvKey(activeWorkspace?.id ?? 'n/a'),
|
key: kvKey(allEnvironments[0]?.workspaceId ?? 'n/a'),
|
||||||
namespace,
|
namespace,
|
||||||
fallback,
|
fallback,
|
||||||
});
|
});
|
||||||
@@ -30,19 +27,16 @@ export function useRecentEnvironments() {
|
|||||||
|
|
||||||
export function useSubscribeRecentEnvironments() {
|
export function useSubscribeRecentEnvironments() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return jotaiStore.sub(activeEnvironmentIdAtom, async () => {
|
return jotaiStore.sub(activeEnvironmentAtom, async () => {
|
||||||
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
const activeEnvironment = jotaiStore.get(activeEnvironmentAtom);
|
||||||
const activeEnvironmentId = jotaiStore.get(activeEnvironmentIdAtom);
|
if (activeEnvironment == null) return;
|
||||||
if (activeWorkspaceId == null) return;
|
|
||||||
if (activeEnvironmentId == null) return;
|
|
||||||
|
|
||||||
const key = kvKey(activeWorkspaceId);
|
|
||||||
|
|
||||||
|
const key = kvKey(activeEnvironment.workspaceId);
|
||||||
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
|
const recentIds = getKeyValue<string[]>({ 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 withoutActiveId = recentIds.filter((id) => id !== activeEnvironment.id);
|
||||||
const value = [activeEnvironmentId, ...withoutActiveId];
|
const value = [activeEnvironment.id, ...withoutActiveId];
|
||||||
await setKeyValue({ namespace, key, value });
|
await setKeyValue({ namespace, key, value });
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { jotaiStore } from '../lib/jotai';
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
|
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
|
||||||
import { activeRequestIdAtom } from './useActiveRequestId';
|
|
||||||
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
import { useAllRequests } from './useAllRequests';
|
import { useAllRequests } from './useAllRequests';
|
||||||
|
import { activeRequestAtom } from './useActiveRequest';
|
||||||
|
|
||||||
const kvKey = (workspaceId: string) => 'recent_requests::' + workspaceId;
|
const kvKey = (workspaceId: string) => 'recent_requests::' + workspaceId;
|
||||||
const namespace = 'global';
|
const namespace = 'global';
|
||||||
@@ -13,10 +11,9 @@ const fallback: string[] = [];
|
|||||||
|
|
||||||
export function useRecentRequests() {
|
export function useRecentRequests() {
|
||||||
const requests = useAllRequests();
|
const requests = useAllRequests();
|
||||||
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom);
|
|
||||||
|
|
||||||
const { set: setRecentRequests, value: recentRequests } = useKeyValue<string[]>({
|
const { set: setRecentRequests, value: recentRequests } = useKeyValue<string[]>({
|
||||||
key: kvKey(activeWorkspaceId ?? 'n/a'),
|
key: kvKey(requests[0]?.workspaceId ?? 'n/a'),
|
||||||
namespace,
|
namespace,
|
||||||
fallback,
|
fallback,
|
||||||
});
|
});
|
||||||
@@ -31,19 +28,17 @@ export function useRecentRequests() {
|
|||||||
|
|
||||||
export function useSubscribeRecentRequests() {
|
export function useSubscribeRecentRequests() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return jotaiStore.sub(activeRequestIdAtom, async () => {
|
return jotaiStore.sub(activeRequestAtom, async () => {
|
||||||
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
const activeRequest = jotaiStore.get(activeRequestAtom);
|
||||||
const activeRequestId = jotaiStore.get(activeRequestIdAtom);
|
if (activeRequest == null) return;
|
||||||
if (activeWorkspaceId == null) return;
|
|
||||||
if (activeRequestId == null) return;
|
|
||||||
|
|
||||||
const key = kvKey(activeWorkspaceId);
|
const key = kvKey(activeRequest.workspaceId);
|
||||||
|
|
||||||
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
|
const recentIds = getKeyValue<string[]>({ 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 withoutActiveId = recentIds.filter((id) => id !== activeRequest.id);
|
||||||
const value = [activeRequestId, ...withoutActiveId];
|
const value = [activeRequest.id, ...withoutActiveId];
|
||||||
await setKeyValue({ namespace, key, value });
|
await setKeyValue({ namespace, key, value });
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
Reference in New Issue
Block a user