diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 12c1a223..42378983 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1178,7 +1178,7 @@ async fn cmd_install_plugin( async fn cmd_create_grpc_request( workspace_id: &str, name: &str, - sort_priority: f32, + sort_priority: f64, folder_id: Option<&str>, app_handle: AppHandle, window: WebviewWindow, diff --git a/src-tauri/yaak-git/bindings/gen_models.ts b/src-tauri/yaak-git/bindings/gen_models.ts index c78b6a60..84a0f770 100644 --- a/src-tauri/yaak-git/bindings/gen_models.ts +++ b/src-tauri/yaak-git/bindings/gen_models.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, sortPriority: number, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-models/bindings/gen_models.ts b/src-tauri/yaak-models/bindings/gen_models.ts index f4bf5f90..6a616e8e 100644 --- a/src-tauri/yaak-models/bindings/gen_models.ts +++ b/src-tauri/yaak-models/bindings/gen_models.ts @@ -14,7 +14,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs"; export type EncryptedKey = { encryptedKey: string, }; -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, sortPriority: number, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-models/migrations/20251031070515_environment-sort-priority.sql b/src-tauri/yaak-models/migrations/20251031070515_environment-sort-priority.sql new file mode 100644 index 00000000..741687d7 --- /dev/null +++ b/src-tauri/yaak-models/migrations/20251031070515_environment-sort-priority.sql @@ -0,0 +1,2 @@ +ALTER TABLE environments + ADD COLUMN sort_priority REAL DEFAULT 0 NOT NULL; diff --git a/src-tauri/yaak-models/src/models.rs b/src-tauri/yaak-models/src/models.rs index e4f96b18..49600ac2 100644 --- a/src-tauri/yaak-models/src/models.rs +++ b/src-tauri/yaak-models/src/models.rs @@ -554,6 +554,7 @@ pub struct Environment { pub parent_id: Option, pub variables: Vec, pub color: Option, + pub sort_priority: f64, } impl UpsertModelInfo for Environment { @@ -591,6 +592,7 @@ impl UpsertModelInfo for Environment { (Color, self.color.into()), (Name, self.name.trim().into()), (Public, self.public.into()), + (SortPriority, self.sort_priority.into()), (Variables, serde_json::to_string(&self.variables)?.into()), ]) } @@ -604,6 +606,7 @@ impl UpsertModelInfo for Environment { EnvironmentIden::Name, EnvironmentIden::Public, EnvironmentIden::Variables, + EnvironmentIden::SortPriority, ] } @@ -626,6 +629,7 @@ impl UpsertModelInfo for Environment { name: row.get("name")?, public: row.get("public")?, variables: serde_json::from_str(variables.as_str()).unwrap_or_default(), + sort_priority: row.get("sort_priority")?, // Deprecated field, but we need to keep it around for a couple of versions // for compatibility because sync/export don't have a schema field @@ -683,7 +687,7 @@ pub struct Folder { pub description: String, pub headers: Vec, pub name: String, - pub sort_priority: f32, + pub sort_priority: f64, } impl UpsertModelInfo for Folder { @@ -1053,7 +1057,7 @@ pub struct WebsocketRequest { pub headers: Vec, pub message: String, pub name: String, - pub sort_priority: f32, + pub sort_priority: f64, pub url: String, pub url_parameters: Vec, } @@ -1488,7 +1492,7 @@ pub struct GrpcRequest { pub method: Option, pub name: String, pub service: Option, - pub sort_priority: f32, + pub sort_priority: f64, pub url: String, } diff --git a/src-tauri/yaak-plugins/bindings/gen_models.ts b/src-tauri/yaak-plugins/bindings/gen_models.ts index b984c7eb..6b2eb5c8 100644 --- a/src-tauri/yaak-plugins/bindings/gen_models.ts +++ b/src-tauri/yaak-plugins/bindings/gen_models.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, sortPriority: number, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-sync/bindings/gen_models.ts b/src-tauri/yaak-sync/bindings/gen_models.ts index c8e6fc16..6a397665 100644 --- a/src-tauri/yaak-sync/bindings/gen_models.ts +++ b/src-tauri/yaak-sync/bindings/gen_models.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, sortPriority: number, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-web/components/ColorIndicator.tsx b/src-web/components/ColorIndicator.tsx index e4bad672..75bb4600 100644 --- a/src-web/components/ColorIndicator.tsx +++ b/src-web/components/ColorIndicator.tsx @@ -4,22 +4,25 @@ import type { CSSProperties } from 'react'; interface Props { color: string | null; onClick?: () => void; + className?: string; } -export function ColorIndicator({ color, onClick }: Props) { +export function ColorIndicator({ color, onClick, className }: Props) { const style: CSSProperties = { backgroundColor: color ?? undefined }; - const className = - 'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent'; + const finalClassName = classNames( + className, + 'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent flex-shrink-0', + ); if (onClick) { return ( - {outerRightSlot} - - setShowContextMenu(null)} - items={[ - { - label: 'Rename', - leftSlot: , - hidden: isBaseEnvironment(environment), - onSelect: async () => { - const name = await showPrompt({ - id: 'rename-environment', - title: 'Rename Environment', - description: ( - <> - Enter a new name for {environment.name} - - ), - label: 'Name', - confirmText: 'Save', - placeholder: 'New Name', - defaultValue: environment.name, - }); - if (name == null) return; - await patchModel(environment, { name }); - }, - }, - { - label: 'Duplicate', - leftSlot: , - hidden: isBaseEnvironment(environment), - onSelect: () => { - duplicateEnvironment?.(environment); - }, - }, - { - label: environment.color ? 'Change Color' : 'Assign Color', - leftSlot: , - hidden: isBaseEnvironment(environment), - onSelect: async () => showColorPicker(environment), - }, - { - label: `Make ${environment.public ? 'Private' : 'Sharable'}`, - leftSlot: , - rightSlot: , - onSelect: async () => { - await patchModel(environment, { public: !environment.public }); - }, - }, - ...((deleteEnvironment - ? [ - { - color: 'danger', - label: 'Delete', - leftSlot: , - onSelect: () => { - deleteEnvironment(environment); - }, - }, - ] - : []) as DropdownItem[]), - ]} - /> + iconSize="sm" + icon="plus_circle" + className="opacity-50 hover:opacity-100" + title="Add Sub-Environment" + onClick={createSubEnvironment} + /> + )} ); } -const sharableTooltip = ( - -); +function ItemInner({ item }: { item: TreeModel }) { + return ( +
+ {item.model === 'environment' && item.public ? ( +
{sharableTooltip}
+ ) : ( + + )} +
{resolvedModelName(item)}
+
+ ); +} + +async function createSubEnvironment() { + const { baseEnvironment } = jotaiStore.get(environmentsBreakdownAtom); + if (baseEnvironment == null) return; + const id = await createSubEnvironmentAndActivate.mutateAsync(baseEnvironment); + return id; +} + +function getEditOptions(item: TreeModel) { + const options: ReturnType['getEditOptions']>> = { + defaultValue: item.name, + placeholder: 'Name', + async onChange(item, name) { + await patchModel(item, { name }); + }, + }; + return options; +} diff --git a/src-web/components/EnvironmentEditor.tsx b/src-web/components/EnvironmentEditor.tsx index 5c4fa694..6059b2db 100644 --- a/src-web/components/EnvironmentEditor.tsx +++ b/src-web/components/EnvironmentEditor.tsx @@ -98,10 +98,10 @@ export function EnvironmentEditor({ }; return ( -
+
- + {!hideName &&
{environment?.name}
} {isEncryptionEnabled ? ( !allVariableAreEncrypted ? ( diff --git a/src-web/components/GitCommitDialog.tsx b/src-web/components/GitCommitDialog.tsx index 65b02179..ac8a5ffc 100644 --- a/src-web/components/GitCommitDialog.tsx +++ b/src-web/components/GitCommitDialog.tsx @@ -31,11 +31,11 @@ interface Props { workspace: Workspace; } -interface TreeNode { +interface CommitTreeNode { model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Environment | Workspace; status: GitStatusEntry; - children: TreeNode[]; - ancestors: TreeNode[]; + children: CommitTreeNode[]; + ancestors: CommitTreeNode[]; } export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { @@ -80,14 +80,14 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { const hasAddedAnything = allEntries.find((e) => e.staged) != null; const hasAnythingToAdd = allEntries.find((e) => e.status !== 'current') != null; - const tree: TreeNode | null = useMemo(() => { - const next = (model: TreeNode['model'], ancestors: TreeNode[]): TreeNode | null => { + const tree: CommitTreeNode | null = useMemo(() => { + const next = (model: CommitTreeNode['model'], ancestors: CommitTreeNode[]): CommitTreeNode | null => { const statusEntry = internalEntries?.find((s) => s.relaPath.includes(model.id)); if (statusEntry == null) { return null; } - const node: TreeNode = { + const node: CommitTreeNode = { model, status: statusEntry, children: [], @@ -128,7 +128,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { return No changes since last commit; } - const checkNode = (treeNode: TreeNode) => { + const checkNode = (treeNode: CommitTreeNode) => { const checked = nodeCheckedStatus(treeNode); const newChecked = checked === 'indeterminate' ? true : !checked; setCheckedAndChildren(treeNode, newChecked, unstage.mutate, add.mutate); @@ -211,9 +211,9 @@ function TreeNodeChildren({ depth, onCheck, }: { - node: TreeNode | null; + node: CommitTreeNode | null; depth: number; - onCheck: (node: TreeNode, checked: boolean) => void; + onCheck: (node: CommitTreeNode, checked: boolean) => void; }) { if (node === null) return null; if (!isNodeRelevant(node)) return null; @@ -318,12 +318,12 @@ function ExternalTreeNode({ ); } -function nodeCheckedStatus(root: TreeNode): CheckboxProps['checked'] { +function nodeCheckedStatus(root: CommitTreeNode): CheckboxProps['checked'] { let numVisited = 0; let numChecked = 0; let numCurrent = 0; - const visitChildren = (n: TreeNode) => { + const visitChildren = (n: CommitTreeNode) => { numVisited += 1; if (n.status.status === 'current') { numCurrent += 1; @@ -347,7 +347,7 @@ function nodeCheckedStatus(root: TreeNode): CheckboxProps['checked'] { } function setCheckedAndChildren( - node: TreeNode, + node: CommitTreeNode, checked: boolean, unstage: (args: { relaPaths: string[] }) => void, add: (args: { relaPaths: string[] }) => void, @@ -355,7 +355,7 @@ function setCheckedAndChildren( const toAdd: string[] = []; const toUnstage: string[] = []; - const next = (node: TreeNode) => { + const next = (node: CommitTreeNode) => { for (const child of node.children) { next(child); } @@ -375,7 +375,7 @@ function setCheckedAndChildren( if (toUnstage.length > 0) unstage({ relaPaths: toUnstage }); } -function isNodeRelevant(node: TreeNode): boolean { +function isNodeRelevant(node: CommitTreeNode): boolean { if (node.status.status !== 'current') { return true; } diff --git a/src-web/components/Overlay.tsx b/src-web/components/Overlay.tsx index f60eac44..34282020 100644 --- a/src-web/components/Overlay.tsx +++ b/src-web/components/Overlay.tsx @@ -54,7 +54,6 @@ export function Overlay({ focusTrapOptions={{ allowOutsideClick: true, // So we can still click toasts and things delayInitialFocus: true, - fallbackFocus: () => containerRef.current!, // always have a target initialFocus: () => // Doing this explicitly seems to work better than the default behavior for some reason containerRef.current?.querySelector( @@ -67,12 +66,11 @@ export function Overlay({ '[tabindex]:not([tabindex="-1"])', '[contenteditable]:not([contenteditable="false"])', ].join(', '), - ) ?? undefined, + ) ?? false, }} > { return jotaiStore.sub(activeIdAtom, () => { const activeId = jotaiStore.get(activeIdAtom); @@ -252,7 +253,7 @@ function Sidebar({ className }: { className?: string }) { }, }, 'sidebar.selected.duplicate': { - priority: 999, + priority: 10, enable, cb: async function (items: SidebarModel[]) { if (items.length === 1) { @@ -285,24 +286,7 @@ function Sidebar({ className }: { className?: string }) { // No children means we're in the root if (child == null) { - return [ - ...getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: null }), - { type: 'separator' }, - { - label: 'Expand All Folders', - leftSlot: , - onSelect: actions['sidebar.expand_all'].cb, - hotKeyAction: 'sidebar.expand_all', - hotKeyLabelOnly: true, - }, - { - label: 'Collapse All Folders', - leftSlot: , - onSelect: actions['sidebar.collapse_all'].cb, - hotKeyAction: 'sidebar.collapse_all', - hotKeyLabelOnly: true, - }, - ]; + return getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: null }); } const workspaces = jotaiStore.get(workspacesAtom); @@ -429,7 +413,7 @@ function Sidebar({ className }: { className?: string }) { } useEffect(() => { - const view = filterRef.current; // your EditorView + const view = filterRef.current; if (!view) return; const ext = filter({ fields: allFields ?? [] }); view.dispatch({ effects: filterLanguageCompartmentRef.current.reconfigure(ext) }); @@ -445,16 +429,15 @@ 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 && ( <> ); diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index 99ec9012..c7fa3097 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -78,7 +78,7 @@ export const Button = forwardRef(function Button // Solids variant === 'solid' && 'border-transparent', - variant === 'solid' && color === 'custom' && 'outline-border-focus', + variant === 'solid' && color === 'custom' && 'focus-visible:outline-2 outline-border-focus', variant === 'solid' && color !== 'custom' && 'enabled:hocus:text-text enabled:hocus:bg-surface-highlight outline-border-subtle', diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index 8db218c1..67e6e81e 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -229,7 +229,6 @@ const BaseInput = forwardRef(function InputBase( (e: KeyboardEvent) => { if (e.key !== 'Enter') return; - console.log('HELLO?'); const form = wrapperRef.current?.closest('form'); if (!isValid || form == null) return; @@ -375,7 +374,7 @@ function EncryptionInput({ security: ReturnType | null; obscured: boolean; error: string | null; - }>({ fieldType: 'encrypted', value: null, security: null, obscured: true, error: null }, [ + }>({ fieldType: 'text', value: null, security: null, obscured: true, error: null }, [ ogForceUpdateKey, ]); diff --git a/src-web/components/core/SplitLayout.tsx b/src-web/components/core/SplitLayout.tsx index 70959dec..6e015fd5 100644 --- a/src-web/components/core/SplitLayout.tsx +++ b/src-web/components/core/SplitLayout.tsx @@ -26,6 +26,7 @@ interface Props { minHeightPx?: number; minWidthPx?: number; layout?: SplitLayoutLayout; + resizeHandleClassName?: string; } const baseProperties = { minWidth: 0 }; @@ -42,6 +43,7 @@ export function SplitLayout({ className, name, layout = 'responsive', + resizeHandleClassName, defaultRatio = 0.5, minHeightPx = 10, minWidthPx = 10, @@ -129,7 +131,10 @@ export function SplitLayout({ <> { root: TreeNode; treeId: string; getItemKey: (item: T) => string; - getContextMenu?: (items: T[]) => Promise; + getContextMenu?: (items: T[]) => ContextMenuProps['items'] | Promise; ItemInner: ComponentType<{ treeId: string; item: T }>; - ItemLeftSlot?: 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; @@ -86,7 +87,8 @@ function TreeInner( onActivate, onDragEnd, ItemInner, - ItemLeftSlot, + ItemLeftSlotInner, + ItemRightSlot, root, treeId, }: TreeProps, @@ -108,6 +110,20 @@ function TreeInner( } }, []); + // Select the first item on first render + useEffect(() => { + const ids = jotaiStore.get(selectedIdsFamily(treeId)); + const fallback = selectableItems[0]; + if (ids.length === 0 && fallback != null) { + jotaiStore.set(selectedIdsFamily(treeId), [fallback.node.item.id]); + jotaiStore.set(focusIdsFamily(treeId), { + anchorId: fallback.node.item.id, + lastId: fallback.node.item.id, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [treeId]); + const handleCloseContextMenu = useCallback(() => { setShowContextMenu(null); }, []); @@ -152,6 +168,7 @@ function TreeInner( // Ensure there's always a tabbable item after render useEffect(() => { requestAnimationFrame(ensureTabbableItem); + ensureTabbableItem(); }); const setSelected = useCallback( @@ -199,12 +216,12 @@ function TreeInner( } else { // If right-clicked an item that was NOT in the multiple-selection, just use that one // Also update the selection with it - jotaiStore.set(selectedIdsFamily(treeId), [item.id]); + setSelected([item.id], false); jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id })); return getContextMenu([item]); } }; - }, [getContextMenu, selectableItems, treeId]); + }, [getContextMenu, selectableItems, setSelected, treeId]); const handleSelect = useCallback['onClick']>>( (item, { shiftKey, metaKey, ctrlKey }) => { @@ -411,6 +428,24 @@ function TreeInner( return; } + 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; + if (item == null) { + return; + } + + const isSameParent = item.parent?.item.id === overSelectableItem.node.parent?.item.id; + if (item.localDrag && !isSameParent) { + return; + } + } + // Root is anything past the end of the list, so set it to the end const hoveringRoot = over.id === root.item.id; if (hoveringRoot) { @@ -423,12 +458,7 @@ function TreeInner( return; } - const selectableItem = selectableItems.find((i) => i.node.item.id === over.id) ?? null; - if (selectableItem == null) { - return; - } - - const node = selectableItem.node; + const node = overSelectableItem.node; const side = computeSideForDragMove(node.item.id, e); const item = node.item; @@ -436,7 +466,7 @@ function TreeInner( 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 = selectableItem.index + (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') { @@ -567,7 +597,8 @@ function TreeInner( onClick: handleClick, getEditOptions, ItemInner, - ItemLeftSlot, + ItemLeftSlotInner, + ItemRightSlot, }; const handleContextMenu = useCallback( diff --git a/src-web/components/core/tree/TreeDragOverlay.tsx b/src-web/components/core/tree/TreeDragOverlay.tsx index 0fdf61fd..5b370e58 100644 --- a/src-web/components/core/tree/TreeDragOverlay.tsx +++ b/src-web/components/core/tree/TreeDragOverlay.tsx @@ -10,11 +10,11 @@ export function TreeDragOverlay({ selectableItems, getItemKey, ItemInner, - ItemLeftSlot, + ItemLeftSlotInner, }: { treeId: string; selectableItems: SelectableTreeNode[]; -} & Pick, 'getItemKey' | 'ItemInner' | 'ItemLeftSlot'>) { +} & Pick, 'getItemKey' | 'ItemInner' | 'ItemLeftSlotInner'>) { const draggingItems = useAtomValue(draggingIdsFamily(treeId)); return ( @@ -23,7 +23,7 @@ export function TreeDragOverlay({ nodes={selectableItems.filter((i) => draggingItems.includes(i.node.item.id))} getItemKey={getItemKey} ItemInner={ItemInner} - ItemLeftSlot={ItemLeftSlot} + ItemLeftSlotInner={ItemLeftSlotInner} forceDepth={0} /> diff --git a/src-web/components/core/tree/TreeItem.tsx b/src-web/components/core/tree/TreeItem.tsx index f5d8cbb5..043350c9 100644 --- a/src-web/components/core/tree/TreeItem.tsx +++ b/src-web/components/core/tree/TreeItem.tsx @@ -24,12 +24,12 @@ export interface TreeItemClickEvent { export type TreeItemProps = Pick< TreeProps, - 'ItemInner' | 'ItemLeftSlot' | 'treeId' | 'getEditOptions' | 'getItemKey' + 'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getEditOptions' | 'getItemKey' > & { node: TreeNode; className?: string; onClick?: (item: T, e: TreeItemClickEvent) => void; - getContextMenu?: (item: T) => Promise; + getContextMenu?: (item: T) => ContextMenuProps['items'] | Promise; depth: number; addRef?: (item: T, n: TreeItemHandle | null) => void; }; @@ -47,7 +47,8 @@ function TreeItem_({ treeId, node, ItemInner, - ItemLeftSlot, + ItemLeftSlotInner, + ItemRightSlot, getContextMenu, onClick, getEditOptions, @@ -135,7 +136,7 @@ function TreeItem_({ }, [node.item.id, treeId]); const handleSubmitNameEdit = useCallback( - async function submitNameEdit(el: HTMLInputElement) { + async (el: HTMLInputElement) => { getEditOptions?.(node.item).onChange(node.item, el.value); onClick?.(node.item, { shiftKey: false, ctrlKey: false, metaKey: false }); // Slight delay for the model to propagate to the local store @@ -243,7 +244,12 @@ function TreeItem_({ setShowContextMenu(null); }, []); - const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({ id: node.item.id }); + const { + attributes, + listeners, + setNodeRef: setDraggableRef, + } = useDraggable({ id: node.item.id, disabled: node.draggable === false }); + const { setNodeRef: setDroppableRef } = useDroppable({ id: node.item.id }); const handlePointerDown = useCallback( @@ -290,7 +296,7 @@ function TreeItem_({
{showContextMenu && ( @@ -301,7 +307,11 @@ function TreeItem_({ /> )} {node.children != null ? ( - + {ItemRightSlot != null ? ( + + ) : ( + + )}
); diff --git a/src-web/components/core/tree/TreeItemList.tsx b/src-web/components/core/tree/TreeItemList.tsx index ed1cca02..d2db7257 100644 --- a/src-web/components/core/tree/TreeItemList.tsx +++ b/src-web/components/core/tree/TreeItemList.tsx @@ -1,4 +1,4 @@ -import type { CSSProperties} from 'react'; +import type { CSSProperties } from 'react'; import { Fragment } from 'react'; import type { SelectableTreeNode } from './common'; import type { TreeProps } from './Tree'; @@ -8,7 +8,7 @@ import { TreeItem } from './TreeItem'; export type TreeItemListProps = Pick< TreeProps, - 'ItemInner' | 'ItemLeftSlot' | 'treeId' | 'getItemKey' | 'getEditOptions' + 'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getItemKey' | 'getEditOptions' > & Pick, 'onClick' | 'getContextMenu'> & { nodes: SelectableTreeNode[]; @@ -20,17 +20,13 @@ export type TreeItemListProps = Pick< export function TreeItemList({ className, - getContextMenu, - getEditOptions, getItemKey, nodes, - onClick, - ItemInner, - ItemLeftSlot, style, treeId, forceDepth, addTreeItemRef, + ...props }: TreeItemListProps) { return (
    @@ -38,16 +34,12 @@ export function TreeItemList({ {nodes.map((child, i) => ( diff --git a/src-web/components/core/tree/common.ts b/src-web/components/core/tree/common.ts index e314ca71..b625befb 100644 --- a/src-web/components/core/tree/common.ts +++ b/src-web/components/core/tree/common.ts @@ -7,6 +7,8 @@ export interface TreeNode { hidden?: boolean; parent: TreeNode | null; depth: number; + draggable?: boolean; + localDrag?: boolean; } export interface SelectableTreeNode { diff --git a/src-web/hooks/useEnvironmentsBreakdown.ts b/src-web/hooks/useEnvironmentsBreakdown.ts index 5480ece4..8ad84e2b 100644 --- a/src-web/hooks/useEnvironmentsBreakdown.ts +++ b/src-web/hooks/useEnvironmentsBreakdown.ts @@ -1,19 +1,25 @@ import { environmentsAtom } from '@yaakapp-internal/models'; -import { useAtomValue } from 'jotai'; -import { useMemo } from 'react'; +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 folderEnvironments = + allEnvironments.filter((e) => e.parentModel === 'folder' && e.parentId != null) ?? []; + + const baseEnvironment = baseEnvironments[0] ?? null; + const otherBaseEnvironments = baseEnvironments.filter((e) => e.id !== baseEnvironment?.id) ?? []; + return { + allEnvironments, + baseEnvironment, + subEnvironments, + folderEnvironments, + otherBaseEnvironments, + baseEnvironments, + }; +}); export function useEnvironmentsBreakdown() { - const allEnvironments = useAtomValue(environmentsAtom); - return useMemo(() => { - const baseEnvironments = allEnvironments.filter((e) => e.parentModel == 'workspace') ?? []; - const subEnvironments = - allEnvironments.filter((e) => e.parentModel === 'environment') ?? []; - const folderEnvironments = - allEnvironments.filter((e) => e.parentModel === 'folder' && e.parentId != null) ?? []; - - const baseEnvironment = baseEnvironments[0] ?? null; - const otherBaseEnvironments = - baseEnvironments.filter((e) => e.id !== baseEnvironment?.id) ?? []; - return { allEnvironments, baseEnvironment, subEnvironments, folderEnvironments, otherBaseEnvironments }; - }, [allEnvironments]); + return useAtomValue(environmentsBreakdownAtom); }