From b32bcb497a238b4a82fe7ce65c5bcc03195ff418 Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Tue, 3 Jun 2025 12:47:40 +0500 Subject: [PATCH 01/10] Exclude partials and custom provided excluded directories from tree generation --- webapp/parse_tree.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/webapp/parse_tree.py b/webapp/parse_tree.py index 200f44bc..6d96b6cc 100644 --- a/webapp/parse_tree.py +++ b/webapp/parse_tree.py @@ -16,6 +16,7 @@ "link": ["meta_copydoc"], } +EXCLUDE_PATHS = ["tutorials", "engage", "blog", "partials"] def is_index(path): return path.name == "index.html" @@ -38,6 +39,15 @@ def is_template(path): return True return False +def is_partial(path): + """ + Return True if the file name starts with an underscore, that indicates it as a partial + + Partials are templates that are not meant to be rendered directly, but + included in other templates. + """ + return path.name.startswith("_") + def append_base_path(base, path_name): """ @@ -204,7 +214,10 @@ def is_valid_page(path, extended_path, is_index=True): - They contain the same extended path as the index html. - They extend from the base html. """ - if is_template(path): + + path = Path(path) + + if not path.is_file() or is_template(path) or is_partial(path): return False if not is_index and extended_path: @@ -223,8 +236,9 @@ def get_extended_path(path): with path.open("r") as f: for line in f.readlines(): # TODO: also match single quotes \' - if match := re.search("{% extends [\"'](.*?)[\"'] %}", line): - return match.group(1) + if ".html" in str(path): + if match := re.search("{% extends [\"'](.*?)[\"'] %}", line): + return match.group(1) def update_tags(tags, new_tags): @@ -256,6 +270,11 @@ def scan_directory(path_name, base=None): node = create_node() node["name"] = path_name.split("/templates", 1)[-1] + # Skip scanning directory if it is in excluded paths + for path in EXCLUDE_PATHS: + if re.search(path, node["name"]): + return node + # We get the relative parent for the path if base is None: base = node_path.absolute() From 3ceb1753e21e5d08e800ab6c4aec26a779fde73e Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Tue, 3 Jun 2025 12:48:37 +0500 Subject: [PATCH 02/10] Store extension of nodes e.g., .html, .md, .dir --- .../2e86822886aa_add_ext_to_webpages.py | 26 +++++++++++++++++++ webapp/models.py | 1 + webapp/parse_tree.py | 5 ++++ webapp/site_repository.py | 1 + 4 files changed, 33 insertions(+) create mode 100644 migrations/versions/2e86822886aa_add_ext_to_webpages.py diff --git a/migrations/versions/2e86822886aa_add_ext_to_webpages.py b/migrations/versions/2e86822886aa_add_ext_to_webpages.py new file mode 100644 index 00000000..ff272c0a --- /dev/null +++ b/migrations/versions/2e86822886aa_add_ext_to_webpages.py @@ -0,0 +1,26 @@ +"""Add ext to webpages + +Revision ID: 2e86822886aa +Revises: ac63c9eebbec +Create Date: 2025-06-03 11:38:42.644973 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2e86822886aa' +down_revision = 'ac63c9eebbec' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "webpages", sa.Column("ext", sa.String(), nullable=True) + ) + + +def downgrade(): + op.drop_column("webpages", "ext") diff --git a/webapp/models.py b/webapp/models.py index f1dd745b..20664b0e 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -101,6 +101,7 @@ class Webpage(db.Model, DateTimeMixin): parent_id: int = Column(Integer, ForeignKey("webpages.id")) owner_id: int = Column(Integer, ForeignKey("users.id")) status: str = Column(Enum(WebpageStatus), default=WebpageStatus.AVAILABLE) + ext: str = Column(String, nullable=True) project = relationship("Project", back_populates="webpages") owner = relationship("User", back_populates="webpages") diff --git a/webapp/parse_tree.py b/webapp/parse_tree.py index 6d96b6cc..89fe745a 100644 --- a/webapp/parse_tree.py +++ b/webapp/parse_tree.py @@ -259,6 +259,7 @@ def create_node(): "description": None, "link": None, "children": [], + "ext": None, } @@ -296,6 +297,9 @@ def scan_directory(path_name, base=None): # Get tags, add as child tags = get_tags_rolling_buffer(index_path) node = update_tags(node, tags) + + else: + node["ext"] = ".dir" # Cycle through other files in this directory for child in node_path.iterdir(): @@ -306,6 +310,7 @@ def scan_directory(path_name, base=None): child, extended_path, is_index=False ): child_tags = get_tags_rolling_buffer(child) + child_tags["ext"] = child.suffix # If the child has no copydocs link, use the parent's link if not child_tags.get("link") and extended_path: child_tags["link"] = get_extended_copydoc( diff --git a/webapp/site_repository.py b/webapp/site_repository.py index f3e3bbf6..47df7b52 100644 --- a/webapp/site_repository.py +++ b/webapp/site_repository.py @@ -305,6 +305,7 @@ def __create_webpage_for_node__( webpage.description = node["description"] webpage.copy_doc_link = node["link"] webpage.parent_id = parent_id + webpage.ext = node["ext"] if webpage.status == WebpageStatus.NEW: webpage.status = WebpageStatus.AVAILABLE From 17f3dce007b2f228455a7d91cc922e49e24ec19b Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Tue, 3 Jun 2025 12:49:40 +0500 Subject: [PATCH 03/10] Disable non-webpage nodes in sidebar tree --- .../Navigation/NavigationElement/NavigationElement.tsx | 6 +++++- static/client/services/api/types/pages.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/static/client/components/Navigation/NavigationElement/NavigationElement.tsx b/static/client/components/Navigation/NavigationElement/NavigationElement.tsx index 197e999a..392f1e23 100644 --- a/static/client/components/Navigation/NavigationElement/NavigationElement.tsx +++ b/static/client/components/Navigation/NavigationElement/NavigationElement.tsx @@ -24,7 +24,11 @@ const NavigationElement = ({ activePageName, page, project, onSelect }: INavigat setExpanded((prevValue) => !prevValue); setChildrenHidden((prevValue) => !prevValue); } else { - onSelect(page.name); + if (page.ext !== ".dir") onSelect(page.name); + else { + setExpanded((prevValue) => !prevValue); + setChildrenHidden((prevValue) => !prevValue); + } } }, [expandButtonRef, page, onSelect], diff --git a/static/client/services/api/types/pages.ts b/static/client/services/api/types/pages.ts index 5b95daad..def3906a 100644 --- a/static/client/services/api/types/pages.ts +++ b/static/client/services/api/types/pages.ts @@ -36,6 +36,7 @@ export interface IPage { name: string; updated_at: string; }; + ext?: string; } export interface IPagesResponse { From 1b387a9b2db788ef7753fa9e49955996a04817d5 Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Tue, 3 Jun 2025 12:51:23 +0500 Subject: [PATCH 04/10] Disable breadcrumbs for non-webpage url paths and vice versa drive-by: modify findPage helper to conditionally return bool or page object --- .../components/Breadcrumbs/Breadcrumbs.tsx | 22 ++++++++++++++++--- static/client/services/tree/pages.ts | 13 ++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/static/client/components/Breadcrumbs/Breadcrumbs.tsx b/static/client/components/Breadcrumbs/Breadcrumbs.tsx index 0f79f0c3..c85e0c00 100644 --- a/static/client/components/Breadcrumbs/Breadcrumbs.tsx +++ b/static/client/components/Breadcrumbs/Breadcrumbs.tsx @@ -4,10 +4,14 @@ import { useLocation, useNavigate } from "react-router-dom"; import { type IBreadcrumb } from "./Breadcrumbs.types"; +import { findPage } from "@/services/tree/pages"; +import { useStore } from "@/store"; + const Breadcrumbs = () => { const location = useLocation(); const [breadcrumbs, setBreadcrumbs] = useState([]); const navigate = useNavigate(); + const selectedProject = useStore((state) => state.selectedProject); useEffect(() => { const pageIndex = location.pathname.indexOf("app/webpage/"); @@ -37,14 +41,26 @@ const Breadcrumbs = () => { [navigate], ); + function isValidPage(path: string) { + if (!selectedProject) return false; + const pageUrl = path.split(`/${selectedProject.name}`)[1]; + if (!pageUrl) return true; // Means it's parent index + const page = findPage(selectedProject.templates, pageUrl, "", true); + return page && typeof page === "object" && "ext" in page && page.ext !== ".dir"; + } + return (
{breadcrumbs.map((bc, index) => ( {index < breadcrumbs.length - 1 ? ( - goToPage(e, bc.link)}> - {bc.name} - + isValidPage(bc.link) ? ( + goToPage(e, bc.link)}> + {bc.name} + + ) : ( + {bc.name} + ) ) : ( {bc.name} )} diff --git a/static/client/services/tree/pages.ts b/static/client/services/tree/pages.ts index 4e4b06ee..48081542 100644 --- a/static/client/services/tree/pages.ts +++ b/static/client/services/tree/pages.ts @@ -1,16 +1,23 @@ import type { IPage } from "@/services/api/types/pages"; // recursively find the page in the tree by the given name (URL) -export function findPage(tree: IPage, pageName: string, prefix: string = ""): boolean { +export function findPage( + tree: IPage, + pageName: string, + prefix: string = "", + returnObject: boolean = false, +): boolean | IPage { const parts = pageName.split("/"); + for (let i = 0; i < tree.children.length; i += 1) { if (tree.children[i].name === `${prefix}/${parts[1]}`) { if (parts.length > 2) { - return findPage(tree.children[i], `/${parts.slice(2).join("/")}`, `${prefix}/${parts[1]}`); + return findPage(tree.children[i], `/${parts.slice(2).join("/")}`, `${prefix}/${parts[1]}`, returnObject); } - return true; + return returnObject ? tree.children[i] : true; } } + return false; } From ec5bc6e7c276abd4da130b80a84a40c56890eeed Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Tue, 3 Jun 2025 13:05:34 +0500 Subject: [PATCH 05/10] Hide non-webpage row items from table view --- static/client/components/Views/TableView/TableViewRowItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/client/components/Views/TableView/TableViewRowItem.tsx b/static/client/components/Views/TableView/TableViewRowItem.tsx index 18b39995..6fcebc96 100644 --- a/static/client/components/Views/TableView/TableViewRowItem.tsx +++ b/static/client/components/Views/TableView/TableViewRowItem.tsx @@ -11,7 +11,7 @@ interface TableViewRowItemProps { const TableViewRowItem: React.FC = ({ page }) => { return ( <> - + {page.ext !== ".dir" && } {page?.children?.map((child) => { return ; })} From bb75c8252df7e0d808e25d08371c62ca9bf75b5d Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Tue, 3 Jun 2025 13:14:10 +0500 Subject: [PATCH 06/10] Removed blog, tutorials and engage from excluded paths --- webapp/parse_tree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/parse_tree.py b/webapp/parse_tree.py index 89fe745a..124b6a25 100644 --- a/webapp/parse_tree.py +++ b/webapp/parse_tree.py @@ -16,7 +16,7 @@ "link": ["meta_copydoc"], } -EXCLUDE_PATHS = ["tutorials", "engage", "blog", "partials"] +EXCLUDE_PATHS = ["partials"] def is_index(path): return path.name == "index.html" From 6fd0da899cb8aad240741900439b0842361dbfd9 Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Wed, 4 Jun 2025 09:34:41 +0500 Subject: [PATCH 07/10] setup QA --- webapp/site_repository.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webapp/site_repository.py b/webapp/site_repository.py index 47df7b52..f6bf31d8 100644 --- a/webapp/site_repository.py +++ b/webapp/site_repository.py @@ -254,7 +254,9 @@ def get_tree_from_db(self): .all() ) # build tree from repository in case DB table is empty - if not webpages or self._has_incomplete_pages(webpages): + # TODO: Revert this line to `if not webpages ...` before merging this PR + # This is only for QA + if True or not webpages or self._has_incomplete_pages(webpages): tree = self.get_new_tree() # otherwise, build tree from DB else: From 4a08f48b4c5b87fcefcf569c5bb21969add7ec3c Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Wed, 4 Jun 2025 09:56:53 +0500 Subject: [PATCH 08/10] Lint python --- webapp/parse_tree.py | 9 ++++++--- webapp/site_repository.py | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/webapp/parse_tree.py b/webapp/parse_tree.py index 124b6a25..1e50ad4e 100644 --- a/webapp/parse_tree.py +++ b/webapp/parse_tree.py @@ -18,6 +18,7 @@ EXCLUDE_PATHS = ["partials"] + def is_index(path): return path.name == "index.html" @@ -39,9 +40,11 @@ def is_template(path): return True return False + def is_partial(path): """ - Return True if the file name starts with an underscore, that indicates it as a partial + Return True if the file name starts with an underscore, + that indicates it as a partial Partials are templates that are not meant to be rendered directly, but included in other templates. @@ -216,7 +219,7 @@ def is_valid_page(path, extended_path, is_index=True): """ path = Path(path) - + if not path.is_file() or is_template(path) or is_partial(path): return False @@ -297,7 +300,7 @@ def scan_directory(path_name, base=None): # Get tags, add as child tags = get_tags_rolling_buffer(index_path) node = update_tags(node, tags) - + else: node["ext"] = ".dir" diff --git a/webapp/site_repository.py b/webapp/site_repository.py index f6bf31d8..0647b015 100644 --- a/webapp/site_repository.py +++ b/webapp/site_repository.py @@ -254,7 +254,8 @@ def get_tree_from_db(self): .all() ) # build tree from repository in case DB table is empty - # TODO: Revert this line to `if not webpages ...` before merging this PR + # TODO: Revert this line to `if not webpages ...` + # before merging this PR # This is only for QA if True or not webpages or self._has_incomplete_pages(webpages): tree = self.get_new_tree() From 8acb94438814b083f68f65ae6193eeeaf6793f5f Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Tue, 17 Jun 2025 18:00:57 +0500 Subject: [PATCH 09/10] Addressed review comments --- .../client/components/Breadcrumbs/Breadcrumbs.tsx | 13 +++++++++++-- webapp/parse_tree.py | 1 - 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/static/client/components/Breadcrumbs/Breadcrumbs.tsx b/static/client/components/Breadcrumbs/Breadcrumbs.tsx index c85e0c00..3e6f22f6 100644 --- a/static/client/components/Breadcrumbs/Breadcrumbs.tsx +++ b/static/client/components/Breadcrumbs/Breadcrumbs.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; +import { Button, Tooltip } from "@canonical/react-components"; import { useLocation, useNavigate } from "react-router-dom"; import { type IBreadcrumb } from "./Breadcrumbs.types"; @@ -59,12 +60,20 @@ const Breadcrumbs = () => { {bc.name} ) : ( - {bc.name} + + + ) ) : ( {bc.name} )} - {index < breadcrumbs.length - 1 &&  / } + {index < breadcrumbs.length - 1 && } ))}
diff --git a/webapp/parse_tree.py b/webapp/parse_tree.py index 1e50ad4e..3609e7e8 100644 --- a/webapp/parse_tree.py +++ b/webapp/parse_tree.py @@ -238,7 +238,6 @@ def get_extended_path(path): """Get the path extended by the file""" with path.open("r") as f: for line in f.readlines(): - # TODO: also match single quotes \' if ".html" in str(path): if match := re.search("{% extends [\"'](.*?)[\"'] %}", line): return match.group(1) From 87c1662ecc2d847b66f7c00903c05ad1bbf7435e Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Tue, 17 Jun 2025 18:04:06 +0500 Subject: [PATCH 10/10] caps text --- static/client/components/Breadcrumbs/Breadcrumbs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/client/components/Breadcrumbs/Breadcrumbs.tsx b/static/client/components/Breadcrumbs/Breadcrumbs.tsx index 3e6f22f6..bebd343e 100644 --- a/static/client/components/Breadcrumbs/Breadcrumbs.tsx +++ b/static/client/components/Breadcrumbs/Breadcrumbs.tsx @@ -65,7 +65,7 @@ const Breadcrumbs = () => { position="btm-center" zIndex={999} > -