Skip to content

Commit dda0e55

Browse files
committed
再起的に階層構造として表示する
1 parent c841754 commit dda0e55

File tree

1 file changed

+73
-97
lines changed

1 file changed

+73
-97
lines changed

src/client/components/SidebarArticles.tsx

Lines changed: 73 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -59,33 +59,84 @@ export const SidebarArticles = ({ items, sortType, articleState }: Props) => {
5959
localStorage.setItem(StorageName[articleState], isDetailsOpen.toString());
6060
}, [isDetailsOpen]);
6161

62-
console.log(items);
63-
64-
const rootGrouped: {
65-
[root: string]: {
66-
direct: ItemViewModel[];
67-
children: { [child: string]: ItemViewModel[] };
68-
};
69-
} = {};
62+
// build recursive tree from item.parent (segments array)
7063
const topLevelItems: ItemViewModel[] = [];
64+
65+
type TreeNode = {
66+
name: string;
67+
items: ItemViewModel[];
68+
children: { [name: string]: TreeNode };
69+
};
70+
71+
const roots: { [name: string]: TreeNode } = {};
72+
73+
const addToTree = (segments: string[], item: ItemViewModel) => {
74+
const rootName = segments[0];
75+
if (!roots[rootName])
76+
roots[rootName] = { name: rootName, items: [], children: {} };
77+
let node = roots[rootName];
78+
const rest = segments.slice(1);
79+
if (rest.length === 0) {
80+
node.items.push(item);
81+
return;
82+
}
83+
for (const seg of rest) {
84+
if (!node.children[seg])
85+
node.children[seg] = { name: seg, items: [], children: {} };
86+
node = node.children[seg];
87+
}
88+
node.items.push(item);
89+
};
90+
7191
items.forEach((item) => {
7292
if (!item.parent || item.parent.length === 0) {
7393
topLevelItems.push(item);
7494
} else {
75-
const root = item.parent[0];
76-
const rest = item.parent.slice(1);
77-
const child = rest.length === 0 ? null : rest.join("/");
78-
if (!rootGrouped[root]) rootGrouped[root] = { direct: [], children: {} };
79-
if (child === null) {
80-
rootGrouped[root].direct.push(item);
81-
} else {
82-
if (!rootGrouped[root].children[child])
83-
rootGrouped[root].children[child] = [];
84-
rootGrouped[root].children[child].push(item);
85-
}
95+
addToTree(item.parent, item);
8696
}
8797
});
8898

99+
const countSubtreeItems = (node: TreeNode): number =>
100+
node.items.length +
101+
Object.values(node.children).reduce((s, c) => s + countSubtreeItems(c), 0);
102+
103+
const renderNode = (node: TreeNode, path: string) => {
104+
const cmp = compare[sortType];
105+
return (
106+
<li key={path}>
107+
<details css={articleDetailsStyle} open>
108+
<summary css={articleSummaryStyle}>
109+
{node.name}
110+
<span css={articleSectionTitleCountStyle}>
111+
{countSubtreeItems(node)}
112+
</span>
113+
</summary>
114+
<ul>
115+
{node.items.sort(cmp).map((item) => (
116+
<li key={item.items_show_path}>
117+
<Link css={articlesListItemStyle} to={item.items_show_path}>
118+
<MaterialSymbol
119+
fill={item.modified && articleState !== "Draft"}
120+
>
121+
note
122+
</MaterialSymbol>
123+
<span css={articleListItemInnerStyle}>
124+
{item.modified && articleState !== "Draft" && "(差分あり) "}
125+
{item.title}
126+
</span>
127+
</Link>
128+
</li>
129+
))}
130+
131+
{Object.values(node.children)
132+
.sort((a, b) => a.name.localeCompare(b.name))
133+
.map((child) => renderNode(child, `${path}/${child.name}`))}
134+
</ul>
135+
</details>
136+
</li>
137+
);
138+
};
139+
89140
return (
90141
<details css={articleDetailsStyle} open={isDetailsOpen}>
91142
<summary css={articleSummaryStyle} onClick={toggleAccordion}>
@@ -110,84 +161,9 @@ export const SidebarArticles = ({ items, sortType, articleState }: Props) => {
110161
</li>
111162
))}
112163

113-
{Object.entries(rootGrouped).map(([root, group]) => (
114-
<li key={root}>
115-
<details css={articleDetailsStyle} open>
116-
<summary css={articleSummaryStyle}>
117-
{root}
118-
<span css={articleSectionTitleCountStyle}>
119-
{group.direct.length +
120-
Object.values(group.children).reduce(
121-
(s, arr) => s + arr.length,
122-
0,
123-
)}
124-
</span>
125-
</summary>
126-
<ul>
127-
{group.direct.length > 0 &&
128-
group.direct.sort(compare[sortType]).map((item) => (
129-
<li key={item.items_show_path}>
130-
<Link
131-
css={articlesListItemStyle}
132-
to={item.items_show_path}
133-
>
134-
<MaterialSymbol
135-
fill={item.modified && articleState !== "Draft"}
136-
>
137-
note
138-
</MaterialSymbol>
139-
<span css={articleListItemInnerStyle}>
140-
{item.modified &&
141-
articleState !== "Draft" &&
142-
"(差分あり) "}
143-
{item.title}
144-
</span>
145-
</Link>
146-
</li>
147-
))}
148-
149-
{Object.entries(group.children).map(
150-
([childPath, groupedItems]) => (
151-
<li key={childPath}>
152-
<details css={articleDetailsStyle} open>
153-
<summary css={articleSummaryStyle}>
154-
{childPath}
155-
<span css={articleSectionTitleCountStyle}>
156-
{groupedItems.length}
157-
</span>
158-
</summary>
159-
<ul>
160-
{groupedItems.sort(compare[sortType]).map((item) => (
161-
<li key={item.items_show_path}>
162-
<Link
163-
css={articlesListItemStyle}
164-
to={item.items_show_path}
165-
>
166-
<MaterialSymbol
167-
fill={
168-
item.modified && articleState !== "Draft"
169-
}
170-
>
171-
note
172-
</MaterialSymbol>
173-
<span css={articleListItemInnerStyle}>
174-
{item.modified &&
175-
articleState !== "Draft" &&
176-
"(差分あり) "}
177-
{item.title}
178-
</span>
179-
</Link>
180-
</li>
181-
))}
182-
</ul>
183-
</details>
184-
</li>
185-
),
186-
)}
187-
</ul>
188-
</details>
189-
</li>
190-
))}
164+
{Object.values(roots)
165+
.sort((a, b) => a.name.localeCompare(b.name))
166+
.map((r) => renderNode(r, r.name))}
191167
</ul>
192168
</details>
193169
);

0 commit comments

Comments
 (0)