Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion static/client/components/Navigation/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import NavigationItems from "./NavigationItems";
import NavigationCollapseToggle from "@/components/Navigation/NavigationCollapseToggle";
import SiteSelector from "@/components/SiteSelector";
import { VIEW_OWNED, VIEW_REVIEWED, VIEW_TABLE, VIEW_TREE } from "@/config";
import { useProjects } from "@/services/api/hooks/projects";
import type { IUser } from "@/services/api/types/users";
import type { TView } from "@/services/api/types/views";
import { useStore } from "@/store";
Expand All @@ -25,6 +26,7 @@ const Navigation = (): JSX.Element => {
state.setView,
state.setExpandedProject,
]);
const { data: projects, isLoading } = useProjects();

const logout = useCallback(() => {
setUser({} as IUser);
Expand Down Expand Up @@ -108,7 +110,7 @@ const Navigation = (): JSX.Element => {
{isViewActive(VIEW_TREE) && (
<>
<SiteSelector />
<NavigationItems />
{!(isLoading || !projects.length) && <NavigationItems />}
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const NavigationElement = ({ activePageName, page, project, onSelect }: INavigat
}, [page, project, location]);

useEffect(() => {
if (page?.children.length) {
if (page?.children?.length) {
setChildren(
page.children.sort((c1, c2) =>
NavigationServices.formatPageName(c1.name) < NavigationServices.formatPageName(c2.name) ? -1 : 1,
Expand Down
11 changes: 4 additions & 7 deletions static/client/components/RequestTaskModal/RequestTaskModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { type ChangeEvent, useCallback, useMemo, useState } from "react";
import React from "react";

import { Button, Input, Modal, RadioInput, Spinner, Textarea, Tooltip, useNotify } from "@canonical/react-components";
import type { QueryObserverResult } from "react-query";
import { useNavigate } from "react-router-dom";

import type { IRequestTaskModalProps } from "./RequestTaskModal.types";
Expand All @@ -11,9 +10,7 @@ import Reporter from "@/components/Reporter";
import config from "@/config";
import { usePages } from "@/services/api/hooks/pages";
import { PagesServices } from "@/services/api/services/pages";
import type { IPagesResponse } from "@/services/api/types/pages";
import { ChangeRequestType, PageStatus } from "@/services/api/types/pages";
import type { IApiBasicError } from "@/services/api/types/query";
import { DatesServices } from "@/services/dates";
import { useStore } from "@/store";

Expand Down Expand Up @@ -69,11 +66,11 @@ const RequestTaskModal = ({
onClose();
if (refetch) {
refetch()
.then((data: QueryObserverResult<IPagesResponse, IApiBasicError>[]) => {
.then((data) => {
if (data?.length) {
const project = data.find((p) => p.data?.data?.name === selectedProject?.name);
if (project && project.data?.data) {
setSelectedProject(project.data.data);
const project = data.find((p) => p.data?.name === selectedProject?.name);
if (project && project.data) {
setSelectedProject(project.data);
}
}
})
Expand Down
10 changes: 5 additions & 5 deletions static/client/components/Search/Search.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@ const checkMatches = (pages: IPage[], value: string, matches: IMatch[], project:
});
};

export const searchForMatches = (value: string, tree: IPagesResponse[]): IMatch[] => {
export const searchForMatches = (value: string, tree: IPagesResponse["data"][]): IMatch[] => {
const matches: IMatch[] = [];

tree.forEach((project) => {
if (project.data.templates.children?.length) {
checkMatches(project.data.templates.children, value, matches, project.data.name);
if (project.templates.children?.length) {
checkMatches(project.templates.children, value, matches, project.name);
}
});

return matches;
};

export const getProjectByName = (
data: IPagesResponse[] | undefined,
data: IPagesResponse["data"][] | undefined,
projectName: string,
): IPagesResponse["data"] | undefined => {
return data?.find((project) => project.data.name === projectName)?.data;
return data?.find((project) => project.name === projectName);
};

export * as SearchServices from "./Search.services";
4 changes: 2 additions & 2 deletions static/client/components/Search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const Search = (): JSX.Element => {

const handleChange = useCallback(
(inputValue: string) => {
if (inputValue.length > 2 && data?.length && data[0]?.data) {
if (inputValue.length > 2 && data?.length && data[0]) {
setMatches(SearchServices.searchForMatches(inputValue, data));
} else if (inputValue.length === 0) {
setMatches([]);
Expand Down Expand Up @@ -63,7 +63,7 @@ const Search = (): JSX.Element => {
<SearchBox
autocomplete="off"
className="l-search-box"
disabled={!(data?.length && data[0]?.data)}
disabled={!(data?.length && data[0])}
onBlur={handleInputBlur}
onChange={handleChange}
placeholder="Search a webpage"
Expand Down
3 changes: 2 additions & 1 deletion static/client/components/Views/TableView/ProjectTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ interface ProjectTitleProps {

function countPages(page: IPage): number {
if (!page) return 0;
return 1 + page.children?.reduce((acc, child) => acc + countPages(child), 0);
const children = page.children ?? [];
return 1 + children?.reduce((acc, child) => acc + countPages(child), 0);
}

const ProjectTitle: React.FC<ProjectTitleProps> = ({ project }) => {
Expand Down
5 changes: 3 additions & 2 deletions static/client/pages/Main/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";

import MainLayout from "@/components/MainLayout";
import NewWebpage from "@/pages/NewWebpage";
import Owned from "@/pages/views/Owned";
import Reviewed from "@/pages/views/Reviewed";
import { useAuth } from "@/services/api/hooks/auth";
Expand All @@ -16,8 +17,7 @@ const Main = (): React.ReactNode => {
function getDynamicRoutes() {
if (!data?.length) return;
return data.map(
(project) =>
project?.data?.templates && RoutesServices.generateRoutes(project.data.name, [project.data.templates]),
(project) => project?.templates && RoutesServices.generateRoutes(project.name, [project.templates]),
);
}

Expand All @@ -27,6 +27,7 @@ const Main = (): React.ReactNode => {
<Route element={<MainLayout />} path="/app">
<Route element={<Owned />} path="views/owned" />
<Route element={<Reviewed />} path="views/reviewed" />
<Route element={<NewWebpage />} path="new-webpage" />
{getDynamicRoutes()}
</Route>
<Route element={<Navigate to="/app" />} path="/" />
Expand Down
45 changes: 34 additions & 11 deletions static/client/pages/NewWebpage/NewWebpage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";

import type { MultiSelectItem } from "@canonical/react-components";
import { Button, Input, Spinner } from "@canonical/react-components";
import { useQueryClient } from "react-query";
import { useNavigate } from "react-router-dom";

import NavigationItems from "@/components/Navigation/NavigationItems";
import OwnerAndReviewers from "@/components/OwnerAndReviewers";
Expand All @@ -11,7 +13,7 @@ import { useQueryParams } from "@/helpers/hooks";
import { usePages } from "@/services/api/hooks/pages";
import { PagesServices } from "@/services/api/services/pages";
import type { IUser } from "@/services/api/types/users";
import { TreeServices } from "@/services/tree/pages";
import { insertPage, TreeServices } from "@/services/tree/pages";
import { useStore } from "@/store";

const errorMessage = "Please specify the URL title";
Expand All @@ -33,7 +35,9 @@ const NewWebpage = (): JSX.Element => {
const [reloading, setReloading] = useState<(typeof LoadingState)[keyof typeof LoadingState]>(LoadingState.INITIAL);

const [selectedProject, setSelectedProject] = useStore((state) => [state.selectedProject, state.setSelectedProject]);
const { data, isFetching, refetch } = usePages(true);
const { data, isFetching } = usePages(true);
const navigate = useNavigate();
const queryClient = useQueryClient();

const queryParams = useQueryParams();

Expand Down Expand Up @@ -80,24 +84,43 @@ const NewWebpage = (): JSX.Element => {
owner,
reviewers,
project: selectedProject.name,
parent: location,
parent: location === "/" ? "" : location,
product_ids: products,
content_jira_id: queryParams.get("content_jira_id") || "",
};
PagesServices.createPage(newPage).then(() => {
// refetch the tree from the backend after a new webpage is added to the database
refetch &&
refetch().then(() => {
setReloading(LoadingState.DONE);
});
PagesServices.createPage(newPage).then(async (response) => {
const new_webpage = response.data.webpage;

if (new_webpage.project && new_webpage.project.name) {
insertPage(new_webpage, queryClient);
const project = data?.find((p) => p.name === new_webpage.project?.name);
if (project) setSelectedProject(project);
navigate(`/app/webpage/${new_webpage.project?.name}${new_webpage.url}`);
} else {
throw new Error("Error creating a new webpage.");
}
});
}
}, [titleValue, owner, selectedProject, location, finalUrl, copyDoc, reviewers, products, queryParams, refetch]);
}, [
titleValue,
owner,
selectedProject,
location,
finalUrl,
copyDoc,
reviewers,
products,
queryParams,
queryClient,
data,
setSelectedProject,
navigate,
]);

// update navigation after new page is added to the tree on the backend
useEffect(() => {
if (!isFetching && reloading === LoadingState.DONE && data?.length && selectedProject) {
const project = data.find((p) => p.data.name === selectedProject.name)?.data;
const project = data.find((p) => p.name === selectedProject.name);
if (project) {
const isNewPageExist = TreeServices.findPage(project.templates, `${location}/${titleValue}`);
if (isNewPageExist) {
Expand Down
4 changes: 2 additions & 2 deletions static/client/pages/Webpage/Webpage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ const Webpage = ({ page, project }: IWebpageProps): JSX.Element => {
}, [page.ext]);

const openGitHub = useCallback(() => {
if (page.children.length) {
if (page.children?.length) {
window.open(`${config.ghLink(project)}${page.name}/index${pageExtension}`);
} else {
window.open(`${config.ghLink(project)}${page.name}${pageExtension}`);
}
}, [page.children.length, page.name, pageExtension, project]);
}, [page.children?.length, page.name, pageExtension, project]);

const createNewPage = useCallback(() => {
setChangeType(ChangeRequestType.NEW_WEBPAGE);
Expand Down
6 changes: 3 additions & 3 deletions static/client/services/api/hooks/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { PagesServices } from "@/services/api/services/pages";
import type { IPagesResponse } from "@/services/api/types/pages";
import type { IApiBasicError, IUseQueryHookRest } from "@/services/api/types/query";

export function usePages(noCache: boolean = false): IUseQueryHookRest<IPagesResponse[]> {
const results = useQueries<UseQueryOptions<IPagesResponse, IApiBasicError>[]>(
export function usePages(noCache: boolean = false): IUseQueryHookRest<IPagesResponse["data"][]> {
const results = useQueries<UseQueryOptions<IPagesResponse["data"], IApiBasicError>[]>(
config.projects.map((project) => {
return {
queryKey: ["pages", project],
queryFn: () => PagesServices.getPages(project, noCache),
queryFn: () => PagesServices.getPages(project, noCache).then((response) => response.data),
staleTime: noCache ? 0 : 300000,
cacheTime: noCache ? 0 : 300000,
refetchOnMount: false,
Expand Down
6 changes: 3 additions & 3 deletions static/client/services/api/hooks/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export function useProjects() {

const [projects, setProjects] = useState<IPagesResponse["data"][]>([]);

const hasData = data?.every((project) => project?.data);
const hasData = data?.every((project) => project!);
if (!isLoading && data?.length && hasData && projects.length !== data.length) {
setProjects(data.map((project) => project.data));
setProjects(data);
}

function filterProjectsAndPages(data: IPagesResponse["data"]) {
Expand Down Expand Up @@ -81,7 +81,7 @@ export function useProjects() {

return {
data: getFilteredProjects(),
unfilteredProjects: data?.map((project) => project?.data),
unfilteredProjects: data,
isLoading,
isFilterApplied,
};
Expand Down
4 changes: 3 additions & 1 deletion static/client/services/api/types/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ export interface INewPage {
}

export interface INewPageResponse {
copy_doc: string;
data: {
webpage: IPage;
};
}

export const ChangeRequestType = {
Expand Down
2 changes: 1 addition & 1 deletion static/client/services/api/types/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface IApiBasicError {
interface IUseQueryHookBase<T extends unknown> {
isLoading?: boolean;
data: T | undefined;
refetch?: () => Promise<QueryObserverResult<IPagesResponse, IApiBasicError>[]>;
refetch?: () => Promise<QueryObserverResult<IPagesResponse["data"], IApiBasicError>[]>;
isFetching?: boolean;
}

Expand Down
2 changes: 0 additions & 2 deletions static/client/services/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from "react";

import { Route } from "react-router-dom";

import NewWebpage from "@/pages/NewWebpage";
import Webpage from "@/pages/Webpage";
import { type IPage } from "@/services/api/types/pages";

Expand All @@ -14,7 +13,6 @@ export function generateRoutes(project: string, pages: IPage[]): JSX.Element[] {
key={page.name}
path={`webpage/${project}${page.name}`}
/>
<Route element={<NewWebpage />} key="new-webpage" path="new-webpage" />
{page.children?.length && generateRoutes(project, page.children)}
</React.Fragment>
));
Expand Down
56 changes: 55 additions & 1 deletion static/client/services/tree/pages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { IPage } from "@/services/api/types/pages";
import type { QueryClient } from "react-query";

import type { IPage, IPagesResponse } from "@/services/api/types/pages";

// recursively find the page in the tree by the given name (URL)
export function findPage(
Expand All @@ -21,4 +23,56 @@ export function findPage(
return false;
}

export function findPageById(pageId: number, tree: IPage): IPage | boolean {
if (tree.id === pageId) return tree;
for (let i = 0; i < tree.children.length; i += 1) {
if (pageId === tree.children[i].id) return tree.children[i];
if (tree.children[i].children?.length) {
const found = findPageById(pageId, tree.children[i]);
if (found) return found;
}
}
return false;
}

export function insertPage(page: IPage, queryClient: QueryClient) {
if (!page || !page.project?.name || !page.id || !page.parent_id) return;

const projectName = page.project.name;

// Access the specific cache entry
const key = ["pages", projectName];
const oldCache = queryClient.getQueryData<IPagesResponse["data"]>(key); // Replace 'any' with your exact type

if (!oldCache) return;

// Check if the page already exists
const pageExists = findPageById(page.id, oldCache.templates);
if (pageExists) return;

// find the parent page to insert the new page under

const parentPage = findPageById(page.parent_id, oldCache.templates) as IPage;

if (!parentPage) return;
if (!parentPage.hasOwnProperty("children")) parentPage.children = [];

// add the new page to the parent's children
parentPage.children.push(page);

// Immutable update: insert page
const updatedTemplates = {
...oldCache.templates,
children: [...oldCache.templates.children],
};

const updatedData = {
...oldCache,
templates: updatedTemplates,
};

// Update the React Query cache
queryClient.setQueryData(key, updatedData);
}

export * as TreeServices from "./pages";
Loading
Loading