Skip to content

Commit f563576

Browse files
committed
Add class inheritance tree
1 parent 9c5db74 commit f563576

2 files changed

Lines changed: 233 additions & 8 deletions

File tree

src/components/Docs/ClassTree.tsx

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { useContext, useMemo, useState } from "react";
2+
import { Link } from "react-router-dom";
3+
import { styled } from "@linaria/react";
4+
import { SchemaClass } from "./api";
5+
import { DeclarationsContext, declarationKey } from "./DeclarationsContext";
6+
7+
interface TreeNode {
8+
cls: SchemaClass;
9+
children: TreeNode[];
10+
}
11+
12+
interface ModuleGroup {
13+
module: string;
14+
roots: TreeNode[];
15+
}
16+
17+
function buildTree(classesByKey: Map<string, SchemaClass>): ModuleGroup[] {
18+
const allNodes = new Map<string, TreeNode>();
19+
const hasParentInTree = new Set<string>();
20+
21+
for (const [key, cls] of classesByKey) {
22+
allNodes.set(key, { cls, children: [] });
23+
}
24+
25+
for (const [key, cls] of classesByKey) {
26+
for (const parent of cls.parents) {
27+
const parentKey = declarationKey(parent.module, parent.name);
28+
const parentNode = allNodes.get(parentKey);
29+
if (parentNode) {
30+
hasParentInTree.add(key);
31+
parentNode.children.push(allNodes.get(key)!);
32+
}
33+
}
34+
}
35+
36+
for (const node of allNodes.values()) {
37+
if (node.children.length > 1) {
38+
node.children.sort((a, b) => a.cls.name.localeCompare(b.cls.name));
39+
}
40+
}
41+
42+
const moduleRoots = new Map<string, TreeNode[]>();
43+
for (const [key, node] of allNodes) {
44+
if (!hasParentInTree.has(key)) {
45+
let roots = moduleRoots.get(node.cls.module);
46+
if (!roots) {
47+
roots = [];
48+
moduleRoots.set(node.cls.module, roots);
49+
}
50+
roots.push(node);
51+
}
52+
}
53+
54+
const groups: ModuleGroup[] = [];
55+
for (const [module, roots] of moduleRoots) {
56+
roots.sort((a, b) => a.cls.name.localeCompare(b.cls.name));
57+
groups.push({ module, roots });
58+
}
59+
groups.sort((a, b) => a.module.localeCompare(b.module));
60+
return groups;
61+
}
62+
63+
function filterTree(nodes: TreeNode[], lower: string): TreeNode[] {
64+
const result: TreeNode[] = [];
65+
for (const node of nodes) {
66+
const nameMatches = node.cls.name.toLowerCase().includes(lower);
67+
if (nameMatches) {
68+
result.push(node);
69+
} else {
70+
const filteredChildren = filterTree(node.children, lower);
71+
if (filteredChildren.length > 0) {
72+
result.push({ cls: node.cls, children: filteredChildren });
73+
}
74+
}
75+
}
76+
return result;
77+
}
78+
79+
function filterGroups(groups: ModuleGroup[], query: string): ModuleGroup[] {
80+
const lower = query.toLowerCase();
81+
const result: ModuleGroup[] = [];
82+
for (const group of groups) {
83+
const filteredRoots = filterTree(group.roots, lower);
84+
if (filteredRoots.length > 0) {
85+
result.push({ module: group.module, roots: filteredRoots });
86+
}
87+
}
88+
return result;
89+
}
90+
91+
const ClassLink = styled(Link)`
92+
text-decoration: none;
93+
color: var(--text);
94+
font-size: 14px;
95+
96+
&:hover {
97+
color: var(--highlight);
98+
}
99+
`;
100+
101+
const TreeList = styled.ul`
102+
margin: 0;
103+
padding-left: 20px;
104+
list-style: none;
105+
`;
106+
107+
const RootList = styled.ul`
108+
margin: 0;
109+
padding: 0;
110+
list-style: none;
111+
`;
112+
113+
const TreeHeader = styled.div`
114+
display: flex;
115+
align-items: center;
116+
justify-content: space-between;
117+
padding: 8px 4px;
118+
font-size: 14px;
119+
color: var(--text-dim);
120+
`;
121+
122+
const TreeFilterInput = styled.input`
123+
background: var(--group-members);
124+
border: 1px solid var(--group-border);
125+
border-radius: 6px;
126+
padding: 4px 10px;
127+
font: inherit;
128+
font-size: 13px;
129+
color: var(--text);
130+
width: 200px;
131+
132+
&:focus {
133+
outline: none;
134+
border-color: var(--highlight);
135+
}
136+
`;
137+
138+
function TreeNodeView({ node, root }: { node: TreeNode; root: string }) {
139+
return (
140+
<li>
141+
<ClassLink to={`${root}/${node.cls.module}/${node.cls.name}`}>{node.cls.name}</ClassLink>
142+
{node.children.length > 0 && (
143+
<TreeList>
144+
{node.children.map((child) => (
145+
<TreeNodeView
146+
key={declarationKey(child.cls.module, child.cls.name)}
147+
node={child}
148+
root={root}
149+
/>
150+
))}
151+
</TreeList>
152+
)}
153+
</li>
154+
);
155+
}
156+
157+
export function ClassTree() {
158+
const { classesByKey, root } = useContext(DeclarationsContext);
159+
const [filter, setFilter] = useState("");
160+
161+
const groups = useMemo(() => buildTree(classesByKey), [classesByKey]);
162+
163+
const displayGroups = useMemo(() => {
164+
if (!filter) return groups;
165+
return filterGroups(groups, filter);
166+
}, [groups, filter]);
167+
168+
const totalClasses = classesByKey.size;
169+
170+
return (
171+
<>
172+
<TreeHeader>
173+
<TreeFilterInput
174+
type="search"
175+
placeholder={`Filter ${totalClasses} classes...`}
176+
value={filter}
177+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilter(e.target.value)}
178+
/>
179+
</TreeHeader>
180+
<RootList>
181+
{displayGroups.map((group) => (
182+
<li key={group.module}>
183+
<strong>{group.module}</strong>
184+
<TreeList>
185+
{group.roots.map((node) => (
186+
<TreeNodeView
187+
key={declarationKey(node.cls.module, node.cls.name)}
188+
node={node}
189+
root={root}
190+
/>
191+
))}
192+
</TreeList>
193+
</li>
194+
))}
195+
</RootList>
196+
</>
197+
);
198+
}

src/components/Docs/ContentList.tsx

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import React, { useContext } from "react";
1+
import React, { useContext, useState } from "react";
22
import { useNavigate } from "react-router-dom";
33
import { styled } from "@linaria/react";
44
import { ContentWrapper, ListItem, TextMessage } from "../layout/Content";
55
import { LazyList, ScrollableList } from "../Lists";
66
import { useFilteredData } from "./utils/filtering";
77
import { SchemaClassView } from "./SchemaClass";
88
import { SchemaEnumView } from "./SchemaEnum";
9+
import { ClassTree } from "./ClassTree";
910
import { Declaration } from "./api";
1011
import { DeclarationsContext } from "./DeclarationsContext";
1112
import { SearchContext } from "../Search/SearchContext";
@@ -138,6 +139,26 @@ function SearchExample({ query }: { query: string }) {
138139
);
139140
}
140141

142+
const TreeToggle = styled.button`
143+
display: block;
144+
max-width: 560px;
145+
margin: 16px auto 0;
146+
padding: 10px 20px;
147+
background: var(--group);
148+
border: 1px solid var(--group-border);
149+
border-radius: 10px;
150+
font: inherit;
151+
font-size: 15px;
152+
color: var(--text);
153+
cursor: pointer;
154+
text-align: center;
155+
width: 100%;
156+
157+
&:hover {
158+
border-color: var(--highlight);
159+
}
160+
`;
161+
141162
const OffsetsNote = styled.div`
142163
font-size: 14px;
143164
color: var(--text-dim);
@@ -163,6 +184,9 @@ export function ContentList() {
163184
const { declarations, metadata, loading, error } = useContext(DeclarationsContext);
164185
const { search } = useContext(SearchContext);
165186
const { data, isSearching } = useFilteredData(declarations);
187+
const [showTree, setShowTree] = useState(
188+
() => /bot|crawl|spider|slurp/i.test(navigator.userAgent) || navigator.webdriver,
189+
);
166190

167191
return (
168192
<ContentWrapper>
@@ -174,15 +198,13 @@ export function ContentList() {
174198
)
175199
) : isSearching ? (
176200
<TextMessage>No results found</TextMessage>
201+
) : error ? (
202+
<TextMessage>{`Failed to load schemas: ${error}`}</TextMessage>
203+
) : loading ? (
204+
<TextMessage>Loading schemas...</TextMessage>
177205
) : (
178206
<>
179-
<TextMessage>
180-
{error
181-
? `Failed to load schemas: ${error}`
182-
: loading
183-
? "Loading schemas..."
184-
: "Choose a class or enum from the sidebar, or use search..."}
185-
</TextMessage>
207+
<TextMessage>Choose a class or enum from the sidebar, or use search...</TextMessage>
186208
<InfoBlock>
187209
<p>
188210
Source 2 includes a schema system that describes the engine's classes, fields, and
@@ -236,6 +258,11 @@ export function ContentList() {
236258
</dl>
237259
Filters can be combined: <SearchExample query="module:client metadata:MNetworkEnable" />
238260
</SearchFiltersBlock>
261+
{showTree ? (
262+
<ClassTree />
263+
) : (
264+
<TreeToggle onClick={() => setShowTree(true)}>View class inheritance tree</TreeToggle>
265+
)}
239266
</>
240267
)}
241268
{data.length > 0 && metadata.revision > 0 && (

0 commit comments

Comments
 (0)