mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-23 18:01:08 +01:00
Use TRee component for Environment dialog (#288)
This commit is contained in:
@@ -51,9 +51,10 @@ export interface TreeProps<T extends { id: string }> {
|
||||
root: TreeNode<T>;
|
||||
treeId: string;
|
||||
getItemKey: (item: T) => string;
|
||||
getContextMenu?: (items: T[]) => Promise<ContextMenuProps['items']>;
|
||||
getContextMenu?: (items: T[]) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
|
||||
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<T extends { id: string }>(
|
||||
onActivate,
|
||||
onDragEnd,
|
||||
ItemInner,
|
||||
ItemLeftSlot,
|
||||
ItemLeftSlotInner,
|
||||
ItemRightSlot,
|
||||
root,
|
||||
treeId,
|
||||
}: TreeProps<T>,
|
||||
@@ -108,6 +110,20 @@ function TreeInner<T extends { id: string }>(
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 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<T extends { id: string }>(
|
||||
// Ensure there's always a tabbable item after render
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(ensureTabbableItem);
|
||||
ensureTabbableItem();
|
||||
});
|
||||
|
||||
const setSelected = useCallback(
|
||||
@@ -199,12 +216,12 @@ function TreeInner<T extends { id: string }>(
|
||||
} 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<NonNullable<TreeItemProps<T>['onClick']>>(
|
||||
(item, { shiftKey, metaKey, ctrlKey }) => {
|
||||
@@ -411,6 +428,24 @@ function TreeInner<T extends { id: string }>(
|
||||
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<T extends { id: string }>(
|
||||
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<T extends { id: string }>(
|
||||
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<T extends { id: string }>(
|
||||
onClick: handleClick,
|
||||
getEditOptions,
|
||||
ItemInner,
|
||||
ItemLeftSlot,
|
||||
ItemLeftSlotInner,
|
||||
ItemRightSlot,
|
||||
};
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
|
||||
@@ -10,11 +10,11 @@ export function TreeDragOverlay<T extends { id: string }>({
|
||||
selectableItems,
|
||||
getItemKey,
|
||||
ItemInner,
|
||||
ItemLeftSlot,
|
||||
ItemLeftSlotInner,
|
||||
}: {
|
||||
treeId: string;
|
||||
selectableItems: SelectableTreeNode<T>[];
|
||||
} & Pick<TreeProps<T>, 'getItemKey' | 'ItemInner' | 'ItemLeftSlot'>) {
|
||||
} & Pick<TreeProps<T>, 'getItemKey' | 'ItemInner' | 'ItemLeftSlotInner'>) {
|
||||
const draggingItems = useAtomValue(draggingIdsFamily(treeId));
|
||||
return (
|
||||
<DragOverlay dropAnimation={null}>
|
||||
@@ -23,7 +23,7 @@ export function TreeDragOverlay<T extends { id: string }>({
|
||||
nodes={selectableItems.filter((i) => draggingItems.includes(i.node.item.id))}
|
||||
getItemKey={getItemKey}
|
||||
ItemInner={ItemInner}
|
||||
ItemLeftSlot={ItemLeftSlot}
|
||||
ItemLeftSlotInner={ItemLeftSlotInner}
|
||||
forceDepth={0}
|
||||
/>
|
||||
</DragOverlay>
|
||||
|
||||
@@ -24,12 +24,12 @@ export interface TreeItemClickEvent {
|
||||
|
||||
export type TreeItemProps<T extends { id: string }> = Pick<
|
||||
TreeProps<T>,
|
||||
'ItemInner' | 'ItemLeftSlot' | 'treeId' | 'getEditOptions' | 'getItemKey'
|
||||
'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getEditOptions' | 'getItemKey'
|
||||
> & {
|
||||
node: TreeNode<T>;
|
||||
className?: string;
|
||||
onClick?: (item: T, e: TreeItemClickEvent) => void;
|
||||
getContextMenu?: (item: T) => Promise<ContextMenuProps['items']>;
|
||||
getContextMenu?: (item: T) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
|
||||
depth: number;
|
||||
addRef?: (item: T, n: TreeItemHandle | null) => void;
|
||||
};
|
||||
@@ -47,7 +47,8 @@ function TreeItem_<T extends { id: string }>({
|
||||
treeId,
|
||||
node,
|
||||
ItemInner,
|
||||
ItemLeftSlot,
|
||||
ItemLeftSlotInner,
|
||||
ItemRightSlot,
|
||||
getContextMenu,
|
||||
onClick,
|
||||
getEditOptions,
|
||||
@@ -135,7 +136,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
}, [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_<T extends { id: string }>({
|
||||
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_<T extends { id: string }>({
|
||||
<div
|
||||
className={classNames(
|
||||
'text-text-subtle',
|
||||
'grid grid-cols-[auto_minmax(0,1fr)] items-center rounded-md',
|
||||
'grid grid-cols-[auto_minmax(0,1fr)_auto] gap-x-2 items-center rounded-md',
|
||||
)}
|
||||
>
|
||||
{showContextMenu && (
|
||||
@@ -301,7 +307,11 @@ function TreeItem_<T extends { id: string }>({
|
||||
/>
|
||||
)}
|
||||
{node.children != null ? (
|
||||
<button tabIndex={-1} className="h-full pl-[0.5rem]" onClick={toggleCollapsed}>
|
||||
<button
|
||||
tabIndex={-1}
|
||||
className="h-full pl-[0.5rem] outline-none"
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
<Icon
|
||||
icon={node.children.length === 0 ? 'dot' : 'chevron_right'}
|
||||
className={classNames(
|
||||
@@ -322,12 +332,12 @@ function TreeItem_<T extends { id: string }>({
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
disabled={editing}
|
||||
className="cursor-default tree-item-inner px-2 focus:outline-none flex items-center gap-2 h-full whitespace-nowrap"
|
||||
className="cursor-default tree-item-inner pr-1 focus:outline-none flex items-center gap-2 h-full whitespace-nowrap"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
tabIndex={isLastSelected ? 0 : -1}
|
||||
>
|
||||
{ItemLeftSlot != null && <ItemLeftSlot treeId={treeId} item={node.item} />}
|
||||
{ItemLeftSlotInner != null && <ItemLeftSlotInner treeId={treeId} item={node.item} />}
|
||||
{getEditOptions != null && editing ? (
|
||||
(() => {
|
||||
const { defaultValue, placeholder } = getEditOptions(node.item);
|
||||
@@ -347,6 +357,11 @@ function TreeItem_<T extends { id: string }>({
|
||||
<ItemInner treeId={treeId} item={node.item} />
|
||||
)}
|
||||
</button>
|
||||
{ItemRightSlot != null ? (
|
||||
<ItemRightSlot treeId={treeId} item={node.item} />
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -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<T extends { id: string }> = Pick<
|
||||
TreeProps<T>,
|
||||
'ItemInner' | 'ItemLeftSlot' | 'treeId' | 'getItemKey' | 'getEditOptions'
|
||||
'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getItemKey' | 'getEditOptions'
|
||||
> &
|
||||
Pick<TreeItemProps<T>, 'onClick' | 'getContextMenu'> & {
|
||||
nodes: SelectableTreeNode<T>[];
|
||||
@@ -20,17 +20,13 @@ export type TreeItemListProps<T extends { id: string }> = Pick<
|
||||
|
||||
export function TreeItemList<T extends { id: string }>({
|
||||
className,
|
||||
getContextMenu,
|
||||
getEditOptions,
|
||||
getItemKey,
|
||||
nodes,
|
||||
onClick,
|
||||
ItemInner,
|
||||
ItemLeftSlot,
|
||||
style,
|
||||
treeId,
|
||||
forceDepth,
|
||||
addTreeItemRef,
|
||||
...props
|
||||
}: TreeItemListProps<T>) {
|
||||
return (
|
||||
<ul role="tree" style={style} className={className}>
|
||||
@@ -38,16 +34,12 @@ export function TreeItemList<T extends { id: string }>({
|
||||
{nodes.map((child, i) => (
|
||||
<Fragment key={getItemKey(child.node.item)}>
|
||||
<TreeItem
|
||||
addRef={addTreeItemRef}
|
||||
treeId={treeId}
|
||||
addRef={addTreeItemRef}
|
||||
node={child.node}
|
||||
ItemInner={ItemInner}
|
||||
ItemLeftSlot={ItemLeftSlot}
|
||||
onClick={onClick}
|
||||
getEditOptions={getEditOptions}
|
||||
getContextMenu={getContextMenu}
|
||||
getItemKey={getItemKey}
|
||||
depth={forceDepth == null ? child.depth : forceDepth}
|
||||
{...props}
|
||||
/>
|
||||
<TreeDropMarker node={child.node} treeId={treeId} index={i + 1} />
|
||||
</Fragment>
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface TreeNode<T extends { id: string }> {
|
||||
hidden?: boolean;
|
||||
parent: TreeNode<T> | null;
|
||||
depth: number;
|
||||
draggable?: boolean;
|
||||
localDrag?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectableTreeNode<T extends { id: string }> {
|
||||
|
||||
Reference in New Issue
Block a user