Skip to content
Merged
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
153 changes: 139 additions & 14 deletions frontend/components/layout/entity-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Sidebar,
SidebarTrigger,
SidebarProvider,
SidebarInput,
} from "@/components/ui/sidebar";
import {
CheckSquare,
Expand All @@ -27,10 +28,11 @@ import {
} from "../ui/collapsible";
import { SearchForm } from "../form/search-form";
import { useEntityStore } from "@/store/entityStore";
import React, { createContext, useContext, useState } from "react";
import React, { createContext, useContext, useState, useCallback } from "react";
import { useUsemapStore } from "@/components/pages/editor-page";
import { Asset } from "@/types/schemas/asset/Asset";
import { AssetNode } from "@/types/asset";
import { useFilteredAssetTree } from "@/hooks/useAssetTree";
import { Entity } from "@/types/schemas/entities/Entity";

interface EntitySidebarContextProps {
searchTerm: string;
Expand All @@ -51,19 +53,93 @@ function useEntitySidebar() {
return context;
}

const getAllEntities = (entityAsset: AssetNode<Entity>) => {
const entities: AssetNode<Entity>[] = [entityAsset];
entityAsset.children.forEach((child) => {
entities.push(...getAllEntities(child));
});
return entities;
};

export function EntitySidebarItemLabel({
handleCheck,
asset,
children,
...props
}: React.ComponentProps<typeof SidebarMenuButton> & {
handleCheck: (e: React.MouseEvent<SVGSVGElement>) => void;
asset: Asset;
asset: AssetNode;
}) {
const checkedEntities = useEntityStore((state) => state.checkedEntities);
const { deleteEntity, updateEntityAssetName } = useUsemapStore(
(state) => state,
);
const [isRenaming, setIsRenaming] = useState(false);
const [inputValue, setInputValue] = useState(asset.name);

const handleSave = useCallback(() => {
try {
updateEntityAssetName(asset.id, inputValue);
} catch (err) {
console.error("Failed to rename asset:", err);
}
}, [asset.id, asset.name, inputValue]);

const handleCancel = useCallback(() => {
console.log("canceledd");
setInputValue(asset.name);
setIsRenaming(false);
}, [asset.name]);

const handleKeydown = useCallback(
(e: React.KeyboardEvent) => {
// rename 모드일 때는 SidebarInput이 키 이벤트를 처리하도록 함
if (isRenaming) return;

switch (e.key) {
case "r":
case "R":
case "F2": {
e.preventDefault();
e.stopPropagation();
setIsRenaming(true);
setInputValue(asset.name);
break;
}
case "Delete":
e.preventDefault();
e.stopPropagation();
getAllEntities(asset).forEach((e) => deleteEntity(e));
break;
}
},
[isRenaming, asset.name],
);

const handleInputKeydown = useCallback(
(e: React.KeyboardEvent) => {
e.stopPropagation();

switch (e.key) {
case "Escape":
e.preventDefault();
handleCancel();
break;
case "Enter":
e.preventDefault();
if (inputValue.trim() && inputValue.trim() !== asset.name) {
handleSave();
} else {
handleCancel();
}
break;
}
},
[inputValue, asset.name, handleCancel, handleSave],
);

return (
<SidebarMenuButton {...props}>
<SidebarMenuButton onKeyDown={handleKeydown} tabIndex={0} {...props}>
{checkedEntities.includes(asset) ? (
<CheckSquare
className="size-5 shrink-0 text-blue"
Expand All @@ -75,25 +151,45 @@ export function EntitySidebarItemLabel({
onClick={handleCheck}
/>
)}
<span className="truncate">{asset.name}</span>
{isRenaming ? (
<SidebarInput
value={inputValue}
onBlur={handleCancel}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleInputKeydown}
autoFocus
></SidebarInput>
) : (
<span className="truncate">{asset.name}</span>
)}
{children}
</SidebarMenuButton>
);
}
export function EntitySidebarItem({
asset,
}: {
asset: Asset & { children?: Asset[] };
}) {

export function EntitySidebarItem({ asset }: { asset: AssetNode }) {
const { setEntity, entity, checkedEntities, setCheckedEntities } =
useEntityStore();

const handleCheck = (e: React.MouseEvent<SVGSVGElement>) => {
e.stopPropagation();
if (checkedEntities.includes(asset)) {
setCheckedEntities(checkedEntities.filter((a) => a !== asset));

const allEntities = getAllEntities(asset);
const allChecked = allEntities.every((entity) =>
checkedEntities.includes(entity),
);

if (allChecked) {
// If all entities are checked, uncheck them all
setCheckedEntities(
checkedEntities.filter((e) => !allEntities.includes(e)),
);
} else {
setCheckedEntities([...checkedEntities, asset]);
// If not all entities are checked, check them all
const newEntities = allEntities.filter(
(entity) => !checkedEntities.includes(entity),
);
setCheckedEntities([...checkedEntities, ...newEntities]);
}
};

Expand Down Expand Up @@ -185,13 +281,42 @@ export function EntitySidebarProvider({

export function EntitySidebar() {
const usemap = useUsemapStore((state) => state.usemap);
const deleteEntity = useUsemapStore((state) => state.deleteEntity);
const { searchTerm, setSearchTerm } = useEntitySidebar();
const { entity, checkedEntities, setCheckedEntities } = useEntityStore();

const tree = useFilteredAssetTree(usemap?.entities || [], searchTerm);

const handleDelete = useCallback(() => {
if (checkedEntities.length > 0) {
checkedEntities.forEach((e) => deleteEntity(e));
}
console.log("Deleting entities:", checkedEntities);
setCheckedEntities([]);
}, [checkedEntities, tree, setCheckedEntities, deleteEntity]);

const handleRename = useCallback(() => {
if (entity) {
// Start rename mode for selected entity
console.log("Renaming entity:", entity);
}
}, [entity]);

const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
switch (event.key) {
case "Delete":
event.preventDefault();
handleDelete();
break;
}
},
[handleDelete, handleRename],
);

return (
<SidebarProvider className="absolute left-2 top-2 flex h-1/3 gap-2">
<Sidebar variant="floating">
<Sidebar variant="floating" onKeyDown={handleKeyDown} tabIndex={0}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
Expand Down
39 changes: 17 additions & 22 deletions frontend/hooks/useAssetTree.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { AssetNode } from "@/types/asset";
import { Asset } from "@/types/schemas/asset/Asset";
import fuzzysort from "fuzzysort";
import { useMemo } from "react";

export function useEntityAssetTree(totalEntities: Asset[]): (Asset & { children?: Asset[] })[] {
export function useEntityAssetTree(totalEntities: Asset[]): AssetNode[] {
const tree = useMemo(() => {
const childrenMap = new Map<number | null, Asset[] & { children?: Asset[] }>();
totalEntities.forEach(entity => {
const childrenMap = new Map<number | null, Asset[]>();
totalEntities.forEach((entity) => {
const key = entity.parent_id ?? -1;
if (!childrenMap.has(key)) childrenMap.set(key, []);
childrenMap.get(key)!.push(entity);
});

const buildTree = (parentId: number | null = -1): Asset[] & { children?: Asset[] }[] => {
return (childrenMap.get(parentId) || []).map((entity) => {
const buildTree = (parentId: number | null = -1): AssetNode[] => {
return (childrenMap.get(parentId) || []).map((entity): AssetNode => {
return {
...entity,
children: buildTree(entity.id),
Expand All @@ -23,20 +24,19 @@ export function useEntityAssetTree(totalEntities: Asset[]): (Asset & { children?
}, [totalEntities]);

return tree;

}

export function useAssetTree(totalAssets: Asset[]): (Asset & { children?: Asset[] })[] {
export function useAssetTree(totalAssets: Asset[]): AssetNode[] {
return useMemo(() => {
const childrenMap = new Map<number | null, Asset[] & { children?: Asset[] }>();
totalAssets.forEach(asset => {
const childrenMap = new Map<number | null, Asset[]>();
totalAssets.forEach((asset) => {
const key = asset.parent_id ?? -1;
if (!childrenMap.has(key)) childrenMap.set(key, []);
childrenMap.get(key)!.push(asset);
});

const buildTree = (parentId: number | null = -1): Asset[] & { children?: Asset[] }[] => {
return (childrenMap.get(parentId) || []).map(asset => ({
const buildTree = (parentId: number | null = -1): AssetNode[] => {
return (childrenMap.get(parentId) || []).map((asset): AssetNode => ({
...asset,
children: buildTree(asset.id),
}));
Expand All @@ -49,12 +49,12 @@ export function useAssetTree(totalAssets: Asset[]): (Asset & { children?: Asset[
export function useFilteredAssetTree(
totalAssets: Asset[],
searchTerm: string,
): (Asset & { children?: Asset[] })[] {
): AssetNode[] {
return useMemo(() => {
const result = fuzzysort.go(searchTerm, totalAssets, {
key: "name",
all: true,
})
});

const matched = result.map((r) => r.obj);
const validIds = new Set<number>();
Expand All @@ -70,24 +70,19 @@ export function useFilteredAssetTree(
});

const filtered = totalAssets.filter((a) => validIds.has(a.id));
const childrenMap = new Map<
number | null,
(Asset & { children?: Asset[] })[]
>();
const childrenMap = new Map<number | null, Asset[]>();
filtered.forEach((asset) => {
const key = asset.parent_id ?? -1;
if (!childrenMap.has(key)) childrenMap.set(key, []);
childrenMap.get(key)!.push(asset);
});

const buildTree = (
parentId: number | null = -1,
): (Asset & { children?: Asset[] })[] =>
(childrenMap.get(parentId) || []).map((asset) => ({
const buildTree = (parentId: number | null = -1): AssetNode[] =>
(childrenMap.get(parentId) || []).map((asset): AssetNode => ({
...asset,
children: buildTree(asset.id),
}));

return buildTree(-1);
}, [totalAssets, searchTerm]);
}
}
10 changes: 5 additions & 5 deletions frontend/store/entityStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Asset } from "@/types/schemas/asset/Asset";
import { AssetNode } from "@/types/asset";
import { create } from "zustand";

interface EntityStore {
entity: Asset | null;
setEntity: (entity: Asset) => void;
checkedEntities: Asset[]
setCheckedEntities: (entities: Asset[]) => void;
entity: AssetNode | null;
setEntity: (entity: AssetNode) => void;
checkedEntities: AssetNode[];
setCheckedEntities: (entities: AssetNode[]) => void;
}

export const useEntityStore = create<EntityStore>((set) => ({
Expand Down
9 changes: 9 additions & 0 deletions frontend/store/mapStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type UsemapActions = {
addEntity: (entity: Asset<Entity>) => void;
deleteEntity: (entity: Asset<Entity>) => void;
fetchUsemap: (mapName: string) => Promise<void>;
updateEntityAssetName: (id: number, name: string) => void;
updateEntity: (id: number, path: string[], value: any) => void;
addAsset: (asset: Asset) => void;
deleteAsset: (asset: Asset) => void;
Expand Down Expand Up @@ -75,6 +76,14 @@ export const createUsemapStore = () => {
// maybe can replaced with delete draft.entities[entity.id]?
}),
})),
updateEntityAssetName: (id: number, name: string) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.entities = draft.entities.map((e) =>
e.id === id ? { ...e, name: name } : e,
);
}),
})),
updateEntity: (id: number, path: string[], value: any) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
Expand Down
3 changes: 2 additions & 1 deletion frontend/types/asset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { AssetSchema } from "./schemas/asset/Asset";

export type Asset<T = any> = z.infer<typeof AssetSchema> & { data?: T | null };
export type Asset<T = any> = z.infer<typeof AssetSchema> & { data?: T | null };
export type AssetNode<T = any> = Asset<T> & { children: AssetNode<T>[] };