Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 51 additions & 81 deletions frontend/package-lock.json

Large diffs are not rendered by default.

39 changes: 36 additions & 3 deletions frontend/src/components/Code/CodeExplorer/CodeExplorer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import SquareIcon from "@mui/icons-material/Square";
import { Box, BoxProps } from "@mui/material";
import * as React from "react";
import { useCallback, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { CodeRead } from "../../../api/openapi/models/CodeRead.ts";
import { useAppDispatch, useAppSelector } from "../../../plugins/ReduxHooks.ts";
import { AnnoActions } from "../../../views/annotation/annoSlice.ts";
import ExportCodesButton from "../../Export/ExportCodesButton.tsx";
import { ITree } from "../../TreeExplorer/ITree.ts";
import TreeExplorer from "../../TreeExplorer/TreeExplorer.tsx";
import { flatTree } from "../../TreeExplorer/TreeUtils.ts";
import { useTreeSortOrder } from "../../../hooks/useTreeSortOrder.ts";
import CodeCreateListItemButton from "../CodeCreateListItemButton.tsx";
import CodeExplorerActionMenu from "./CodeExplorerActionMenu.tsx";
import CodeExplorerNodeRenderer from "./CodeExplorerNodeRenderer.tsx";
Expand All @@ -16,9 +18,13 @@ import useComputeCodeTree from "./useComputeCodeTree.ts";
const renderNode = (node: ITree<CodeRead>) => <CodeExplorerNodeRenderer node={node} />;
const renderActions = (node: ITree<CodeRead>) => <CodeExplorerActionMenu node={node} />;

function CodeExplorer(props: BoxProps) {
interface CodeExplorerProps {
projectId?: number;
}

function CodeExplorer({ projectId, ...props }: CodeExplorerProps & BoxProps) {
// custom hooks
const { codeTree } = useComputeCodeTree();
const { codeTree, allCodes } = useComputeCodeTree();

// global client state (redux)
const selectedCodeId = useAppSelector((state) => state.annotations.selectedCodeId);
Expand All @@ -28,6 +34,29 @@ function CodeExplorer(props: BoxProps) {
// local client state
const [codeFilter, setCodeFilter] = useState<string>("");

// Get all code IDs from the tree
const allCodeIds = useMemo(() => {
if (!codeTree) return [];
return flatTree(codeTree.model).map((code) => code.id);
}, [codeTree]);

// Extract projectId from data for dependency tracking
const dataProjectId = allCodes.data?.[0]?.project_id;

// Use project ID from props or derive from data (fallback)
// Note: In practice, all codes belong to the same project (enforced by backend)
// Ideally, projectId should be passed as a prop from parent components
const effectiveProjectId = useMemo(() => {
return projectId ?? dataProjectId;
}, [projectId, dataProjectId]);

// Use custom sort order hook
const { sortOrder, updateSortOrder } = useTreeSortOrder(
"code-sort-order",
effectiveProjectId,
allCodeIds
);

// handle ui events
const handleExpandedCodeIdsChange = useCallback(
(newCodeIds: string[]) => {
Expand Down Expand Up @@ -72,6 +101,10 @@ function CodeExplorer(props: BoxProps) {
renderActions={renderActions}
// components
listActions={<ListActions />}
// drag and drop for reordering
draggableItems={true}
sortOrder={sortOrder}
onSortOrderChange={updateSortOrder}
/>
)}
</Box>
Expand Down
36 changes: 33 additions & 3 deletions frontend/src/components/Tag/TagExplorer/TagExplorer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import LabelIcon from "@mui/icons-material/Label";
import { Box, BoxProps } from "@mui/material";
import { memo, useCallback, useState } from "react";
import { memo, useCallback, useMemo, useState } from "react";
import { TagRead } from "../../../api/openapi/models/TagRead.ts";
import { useAppDispatch, useAppSelector } from "../../../plugins/ReduxHooks.ts";
import { SearchActions } from "../../../views/search/DocumentSearch/searchSlice.ts";
import ExportTagsButton from "../../Export/ExportTagsButton.tsx";
import { ITree } from "../../TreeExplorer/ITree.ts";
import TreeExplorer from "../../TreeExplorer/TreeExplorer.tsx";
import { flatTree } from "../../TreeExplorer/TreeUtils.ts";
import { useTreeSortOrder } from "../../../hooks/useTreeSortOrder.ts";
import TagMenuCreateButton from "../TagMenu/TagMenuCreateButton.tsx";
import TagExplorerActionMenu from "./TagExplorerActionMenu.tsx";
import useComputeTagTree from "./useComputeTagTree.ts";
Expand All @@ -15,11 +17,12 @@ const renderActions = (node: ITree<TagRead>) => <TagExplorerActionMenu node={nod

interface TagExplorerProps {
onTagClick?: (tagId: number) => void;
projectId?: number;
}

function TagExplorer({ onTagClick, ...props }: TagExplorerProps & BoxProps) {
function TagExplorer({ onTagClick, projectId, ...props }: TagExplorerProps & BoxProps) {
// custom hooks
const { tagTree } = useComputeTagTree();
const { tagTree, allTags } = useComputeTagTree();

// tag expansion
const dispatch = useAppDispatch();
Expand All @@ -45,6 +48,29 @@ function TagExplorer({ onTagClick, ...props }: TagExplorerProps & BoxProps) {
[onTagClick],
);

// Get all tag IDs from the tree
const allTagIds = useMemo(() => {
if (!tagTree) return [];
return flatTree(tagTree.model).map((tag) => tag.id);
}, [tagTree]);

// Extract projectId from data for dependency tracking
const dataProjectId = allTags.data?.[0]?.project_id;

// Use project ID from props or derive from data (fallback)
// Note: In practice, all tags belong to the same project (enforced by backend)
// Ideally, projectId should be passed as a prop from parent components
const effectiveProjectId = useMemo(() => {
return projectId ?? dataProjectId;
}, [projectId, dataProjectId]);

// Use custom sort order hook
const { sortOrder, updateSortOrder } = useTreeSortOrder(
"tag-sort-order",
effectiveProjectId,
allTagIds
);

return (
<Box {...props}>
{tagTree && (
Expand All @@ -66,6 +92,10 @@ function TagExplorer({ onTagClick, ...props }: TagExplorerProps & BoxProps) {
renderActions={renderActions}
// components
listActions={<ListActions />}
// drag and drop for reordering
draggableItems={true}
sortOrder={sortOrder}
onSortOrderChange={updateSortOrder}
/>
)}
</Box>
Expand Down
44 changes: 33 additions & 11 deletions frontend/src/components/TreeExplorer/DataTreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AbcIcon from "@mui/icons-material/Abc";
import { Typography } from "@mui/material";
import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView";
import { useCallback } from "react";
import { Draggable } from "../DnD/Draggable.tsx";
import Droppable from "../DnD/Droppable.tsx";
import { ITree, NamedObjWithParent } from "./ITree.ts";

Expand All @@ -24,6 +25,7 @@ export interface DataTreeViewProps<T extends NamedObjWithParent> {
parentIcon?: React.ElementType<SvgIconProps>;
droppable?: boolean | ((node: ITree<T>) => boolean);
droppableId?: (node: ITree<T>) => string;
draggable?: boolean;
}

const defaultNodeRenderer = <T extends NamedObjWithParent>(node: ITree<T>) => (
Expand All @@ -39,6 +41,7 @@ function DataTreeView<T extends NamedObjWithParent>({
dataIcon,
droppable,
droppableId,
draggable = false,
renderRoot = false,
disableRootActions = false,
rootIcon = FolderIcon,
Expand All @@ -52,13 +55,40 @@ function DataTreeView<T extends NamedObjWithParent>({
// Use rootIcon for the root node if provided and isRoot is true
const iconToUse = isRoot ? rootIcon : hasChildren ? parentIcon : dataIcon ? dataIcon : AbcIcon;

const label = (
const labelContent = (
<Box sx={{ display: "flex", alignItems: "center", p: 0.5, pr: 0 }}>
<Box component={iconToUse} color={node.data.color} sx={{ mr: 1 }} />
{renderNode(node)}
{renderActions && !(isRoot && disableRootActions) ? renderActions(node) : undefined}
</Box>
);

// Wrap with droppable if needed
const droppableWrapped = (typeof droppable === "function" ? droppable(node) : droppable) ? (
<Droppable id={droppableId ? droppableId(node) : `folder-${node.data.id}`} Element="div">
{labelContent}
</Droppable>
) : (
labelContent
);

// Wrap with draggable if needed
const label = draggable && !isRoot ? (
<Draggable
id={`tree-item-${node.data.id}`}
data={{
type: "tree-item",
id: node.data.id,
parentId: node.data.parent_id,
}}
Element="div"
>
{droppableWrapped}
</Draggable>
) : (
droppableWrapped
);

return (
<TreeItem
key={node.data.id}
Expand All @@ -67,22 +97,14 @@ function DataTreeView<T extends NamedObjWithParent>({
expandIcon: ArrowRightIcon,
collapseIcon: ArrowDropDownIcon,
}}
label={
(typeof droppable === "function" ? droppable(node) : droppable) ? (
<Droppable id={droppableId ? droppableId(node) : `folder-${node.data.id}`} Element="div">
{label}
</Droppable>
) : (
label
)
}
label={label}
>
{hasChildren && <React.Fragment> {renderTree(node.children!, false)} </React.Fragment>}
</TreeItem>
);
});
},
[rootIcon, parentIcon, dataIcon, renderNode, renderActions, disableRootActions, droppable, droppableId],
[rootIcon, parentIcon, dataIcon, renderNode, renderActions, disableRootActions, droppable, droppableId, draggable],
);

return (
Expand Down
Loading