Skip to content

Commit eeaf097

Browse files
Merge pull request #200 from canonical/WD-22062
[WD-22062] Make non-webpage nodes unclickable and exclude partials
2 parents 43600ef + 880047a commit eeaf097

File tree

9 files changed

+108
-14
lines changed

9 files changed

+108
-14
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Add ext to webpages
2+
3+
Revision ID: 2e86822886aa
4+
Revises: ac63c9eebbec
5+
Create Date: 2025-06-03 11:38:42.644973
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '2e86822886aa'
14+
down_revision = 'ac63c9eebbec'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.add_column(
21+
"webpages", sa.Column("ext", sa.String(), nullable=True)
22+
)
23+
24+
25+
def downgrade():
26+
op.drop_column("webpages", "ext")

static/client/components/Breadcrumbs/Breadcrumbs.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import React, { useEffect, useState } from "react";
22

3+
import { Button, Tooltip } from "@canonical/react-components";
34
import { useLocation, useNavigate } from "react-router-dom";
45

56
import { type IBreadcrumb } from "./Breadcrumbs.types";
67

8+
import { findPage } from "@/services/tree/pages";
9+
import { useStore } from "@/store";
10+
711
const Breadcrumbs = () => {
812
const location = useLocation();
913
const [breadcrumbs, setBreadcrumbs] = useState<IBreadcrumb[]>([]);
1014
const navigate = useNavigate();
15+
const selectedProject = useStore((state) => state.selectedProject);
1116

1217
useEffect(() => {
1318
const pageIndex = location.pathname.indexOf("app/webpage/");
@@ -37,18 +42,38 @@ const Breadcrumbs = () => {
3742
[navigate],
3843
);
3944

45+
function isValidPage(path: string) {
46+
if (!selectedProject) return false;
47+
const pageUrl = path.split(`/${selectedProject.name}`)[1];
48+
if (!pageUrl) return true; // Means it's parent index
49+
const page = findPage(selectedProject.templates, pageUrl, "", true);
50+
return page && typeof page === "object" && "ext" in page && page.ext !== ".dir";
51+
}
52+
4053
return (
4154
<div className="l-breadcrumbs">
4255
{breadcrumbs.map((bc, index) => (
4356
<React.Fragment key={`bc-${index}`}>
4457
{index < breadcrumbs.length - 1 ? (
45-
<a className="p-text--small-caps" href={bc.link} onClick={(e) => goToPage(e, bc.link)}>
46-
{bc.name}
47-
</a>
58+
isValidPage(bc.link) ? (
59+
<a className="p-text--small-caps" href={bc.link} onClick={(e) => goToPage(e, bc.link)}>
60+
{bc.name}
61+
</a>
62+
) : (
63+
<Tooltip
64+
message="This part of the path isn't a page and can't be opened"
65+
position="btm-center"
66+
zIndex={999}
67+
>
68+
<Button className="p-button--base p-text--small-caps u-no-margin u-no-padding" disabled>
69+
{bc.name}
70+
</Button>
71+
</Tooltip>
72+
)
4873
) : (
4974
<span className="p-text--small-caps">{bc.name}</span>
5075
)}
51-
{index < breadcrumbs.length - 1 && <span>&nbsp;/&nbsp;</span>}
76+
{index < breadcrumbs.length - 1 && <span aria-hidden="true">&nbsp;/&nbsp;</span>}
5277
</React.Fragment>
5378
))}
5479
</div>

static/client/components/Navigation/NavigationElement/NavigationElement.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ const NavigationElement = ({ activePageName, page, project, onSelect }: INavigat
2424
setExpanded((prevValue) => !prevValue);
2525
setChildrenHidden((prevValue) => !prevValue);
2626
} else {
27-
onSelect(page.name);
27+
if (page.ext !== ".dir") onSelect(page.name);
28+
else {
29+
setExpanded((prevValue) => !prevValue);
30+
setChildrenHidden((prevValue) => !prevValue);
31+
}
2832
}
2933
},
3034
[expandButtonRef, page, onSelect],

static/client/components/Views/TableView/TableViewRowItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface TableViewRowItemProps {
1111
const TableViewRowItem: React.FC<TableViewRowItemProps> = ({ page }) => {
1212
return (
1313
<>
14-
<TableViewRow page={page} />
14+
{page.ext !== ".dir" && <TableViewRow page={page} />}
1515
{page?.children?.map((child) => {
1616
return <TableViewRowItem key={`${child.project?.name}${child.url}`} page={child} />;
1717
})}

static/client/services/api/types/pages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface IPage {
3636
name: string;
3737
updated_at: string;
3838
};
39+
ext?: string;
3940
}
4041

4142
export interface IPagesResponse {

static/client/services/tree/pages.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import type { IPage } from "@/services/api/types/pages";
22

33
// recursively find the page in the tree by the given name (URL)
4-
export function findPage(tree: IPage, pageName: string, prefix: string = ""): boolean {
4+
export function findPage(
5+
tree: IPage,
6+
pageName: string,
7+
prefix: string = "",
8+
returnObject: boolean = false,
9+
): boolean | IPage {
510
const parts = pageName.split("/");
11+
612
for (let i = 0; i < tree.children.length; i += 1) {
713
if (tree.children[i].name === `${prefix}/${parts[1]}`) {
814
if (parts.length > 2) {
9-
return findPage(tree.children[i], `/${parts.slice(2).join("/")}`, `${prefix}/${parts[1]}`);
15+
return findPage(tree.children[i], `/${parts.slice(2).join("/")}`, `${prefix}/${parts[1]}`, returnObject);
1016
}
11-
return true;
17+
return returnObject ? tree.children[i] : true;
1218
}
1319
}
20+
1421
return false;
1522
}
1623

webapp/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class Webpage(db.Model, DateTimeMixin):
9898
parent_id: int = Column(Integer, ForeignKey("webpages.id"))
9999
owner_id: int = Column(Integer, ForeignKey("users.id"))
100100
status: str = Column(Enum(WebpageStatus), default=WebpageStatus.AVAILABLE)
101+
ext: str = Column(String, nullable=True)
101102

102103
project = relationship("Project", back_populates="webpages")
103104
owner = relationship("User", back_populates="webpages")

webapp/parse_tree.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"link": ["meta_copydoc"],
1717
}
1818

19+
EXCLUDE_PATHS = ["partials"]
20+
1921

2022
def is_index(path):
2123
return path.name == "index.html"
@@ -39,6 +41,17 @@ def is_template(path):
3941
return False
4042

4143

44+
def is_partial(path):
45+
"""
46+
Return True if the file name starts with an underscore,
47+
that indicates it as a partial
48+
49+
Partials are templates that are not meant to be rendered directly, but
50+
included in other templates.
51+
"""
52+
return path.name.startswith("_")
53+
54+
4255
def append_base_path(base, path_name):
4356
"""
4457
Add the base (root) to a path URI.
@@ -204,7 +217,10 @@ def is_valid_page(path, extended_path, is_index=True):
204217
- They contain the same extended path as the index html.
205218
- They extend from the base html.
206219
"""
207-
if is_template(path):
220+
221+
path = Path(path)
222+
223+
if not path.is_file() or is_template(path) or is_partial(path):
208224
return False
209225

210226
if not is_index and extended_path:
@@ -222,9 +238,9 @@ def get_extended_path(path):
222238
"""Get the path extended by the file"""
223239
with path.open("r") as f:
224240
for line in f.readlines():
225-
# TODO: also match single quotes \'
226-
if match := re.search("{% extends [\"'](.*?)[\"'] %}", line):
227-
return match.group(1)
241+
if ".html" in str(path):
242+
if match := re.search("{% extends [\"'](.*?)[\"'] %}", line):
243+
return match.group(1)
228244

229245

230246
def update_tags(tags, new_tags):
@@ -245,6 +261,7 @@ def create_node():
245261
"description": None,
246262
"link": None,
247263
"children": [],
264+
"ext": None,
248265
}
249266

250267

@@ -256,6 +273,11 @@ def scan_directory(path_name, base=None):
256273
node = create_node()
257274
node["name"] = path_name.split("/templates", 1)[-1]
258275

276+
# Skip scanning directory if it is in excluded paths
277+
for path in EXCLUDE_PATHS:
278+
if re.search(path, node["name"]):
279+
return node
280+
259281
# We get the relative parent for the path
260282
if base is None:
261283
base = node_path.absolute()
@@ -278,6 +300,9 @@ def scan_directory(path_name, base=None):
278300
tags = get_tags_rolling_buffer(index_path)
279301
node = update_tags(node, tags)
280302

303+
else:
304+
node["ext"] = ".dir"
305+
281306
# Cycle through other files in this directory
282307
for child in node_path.iterdir():
283308
# If the child is a file, check if it is a valid page
@@ -287,6 +312,7 @@ def scan_directory(path_name, base=None):
287312
child, extended_path, is_index=False
288313
):
289314
child_tags = get_tags_rolling_buffer(child)
315+
child_tags["ext"] = child.suffix
290316
# If the child has no copydocs link, use the parent's link
291317
if not child_tags.get("link") and extended_path:
292318
child_tags["link"] = get_extended_copydoc(

webapp/site_repository.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,10 @@ def get_tree_from_db(self):
255255
.all()
256256
)
257257
# build tree from repository in case DB table is empty
258-
if not webpages or self._has_incomplete_pages(webpages):
258+
# TODO: Revert this line to `if not webpages ...`
259+
# before merging this PR
260+
# This is only for QA
261+
if True or not webpages or self._has_incomplete_pages(webpages):
259262
tree = self.get_new_tree()
260263
# otherwise, build tree from DB
261264
else:
@@ -306,6 +309,7 @@ def __create_webpage_for_node__(
306309
webpage.description = node["description"]
307310
webpage.copy_doc_link = node["link"]
308311
webpage.parent_id = parent_id
312+
webpage.ext = node["ext"]
309313
if webpage.status == WebpageStatus.NEW:
310314
webpage.status = WebpageStatus.AVAILABLE
311315

0 commit comments

Comments
 (0)