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/static/client/components/Breadcrumbs/Breadcrumbs.tsx b/static/client/components/Breadcrumbs/Breadcrumbs.tsx index 0f79f0c3..bebd343e 100644 --- a/static/client/components/Breadcrumbs/Breadcrumbs.tsx +++ b/static/client/components/Breadcrumbs/Breadcrumbs.tsx @@ -1,13 +1,18 @@ 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"; +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,18 +42,38 @@ 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} )} - {index < breadcrumbs.length - 1 &&  / } + {index < breadcrumbs.length - 1 && } ))}
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/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 ; })} 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 { 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; } diff --git a/webapp/models.py b/webapp/models.py index ac505c50..998b0491 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -98,6 +98,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 200f44bc..3609e7e8 100644 --- a/webapp/parse_tree.py +++ b/webapp/parse_tree.py @@ -16,6 +16,8 @@ "link": ["meta_copydoc"], } +EXCLUDE_PATHS = ["partials"] + def is_index(path): return path.name == "index.html" @@ -39,6 +41,17 @@ def is_template(path): 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): """ Add the base (root) to a path URI. @@ -204,7 +217,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: @@ -222,9 +238,9 @@ 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 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): @@ -245,6 +261,7 @@ def create_node(): "description": None, "link": None, "children": [], + "ext": None, } @@ -256,6 +273,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() @@ -278,6 +300,9 @@ def scan_directory(path_name, base=None): 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(): # If the child is a file, check if it is a valid page @@ -287,6 +312,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 7f0a1621..bb93f106 100644 --- a/webapp/site_repository.py +++ b/webapp/site_repository.py @@ -255,7 +255,10 @@ 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: @@ -306,6 +309,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