mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-10-22 20:25:00 +00:00
feat: preserve links to packages
This changes url handling in status page. Firstly, instead of using hash for repository, now they are handled by query parameter. Secondly, it adds a query parameter for package base. And lastly, it changes behaviour for clicking on the package link, now it opens status page, which allows to e.g. open a tab in background
This commit is contained in:
1 parent
eafa1b417a
commit
f776b33310
4 files changed
+87
-21
No files matched your search
@@ -37,7 +37,9 @@ import PackageRebuildDialog from "components/dialogs/PackageRebuildDialog";
|
|||||||
import PackageTableToolbar from "components/table/PackageTableToolbar";
|
import PackageTableToolbar from "components/table/PackageTableToolbar";
|
||||||
import StatusCell from "components/table/StatusCell";
|
import StatusCell from "components/table/StatusCell";
|
||||||
import { useDebounce } from "hooks/useDebounce";
|
import { useDebounce } from "hooks/useDebounce";
|
||||||
|
import { navigate, packageHref } from "hooks/useLocationQuery";
|
||||||
import { usePackageTable } from "hooks/usePackageTable";
|
import { usePackageTable } from "hooks/usePackageTable";
|
||||||
|
import { useRepository } from "hooks/useRepository";
|
||||||
import type { PackageRow } from "models/PackageRow";
|
import type { PackageRow } from "models/PackageRow";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ function createListColumn(
|
|||||||
|
|
||||||
export default function PackageTable(): React.JSX.Element {
|
export default function PackageTable(): React.JSX.Element {
|
||||||
const table = usePackageTable();
|
const table = usePackageTable();
|
||||||
|
const { currentRepository } = useRepository();
|
||||||
const { handleRemove, handleUpdate, isAuthorized } = table;
|
const { handleRemove, handleUpdate, isAuthorized } = table;
|
||||||
const apiRef = useGridApiRef();
|
const apiRef = useGridApiRef();
|
||||||
const debouncedSearch = useDebounce(table.searchText, 300);
|
const debouncedSearch = useDebounce(table.searchText, 300);
|
||||||
@@ -79,11 +82,20 @@ export default function PackageTable(): React.JSX.Element {
|
|||||||
headerName: "package base",
|
headerName: "package base",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
renderCell: params =>
|
renderCell: params =>
|
||||||
params.row.webUrl ?
|
<Link
|
||||||
<Link href={params.row.webUrl} rel="noopener noreferrer" target="_blank" underline="hover">
|
href={packageHref(currentRepository?.key ?? "", params.row.base)}
|
||||||
{params.value as string}
|
onClick={event => {
|
||||||
</Link>
|
const modified = event.ctrlKey || event.metaKey || event.shiftKey || event.altKey;
|
||||||
: params.value as string,
|
if (event.button === 0 && !modified) {
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(packageHref(currentRepository?.key ?? "", params.row.base));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={params.tabIndex}
|
||||||
|
underline="hover"
|
||||||
|
>
|
||||||
|
{params.value as string}
|
||||||
|
</Link>,
|
||||||
},
|
},
|
||||||
{ align: "right", field: "version", headerAlign: "right", headerName: "version", width: 180 },
|
{ align: "right", field: "version", headerAlign: "right", headerName: "version", width: 180 },
|
||||||
createListColumn("packages", "packages", { flex: 1, minWidth: 120 }),
|
createListColumn("packages", "packages", { flex: 1, minWidth: 120 }),
|
||||||
@@ -124,7 +136,7 @@ export default function PackageTable(): React.JSX.Element {
|
|||||||
],
|
],
|
||||||
} satisfies GridColDef<PackageRow>] : [],
|
} satisfies GridColDef<PackageRow>] : [],
|
||||||
],
|
],
|
||||||
[handleRemove, handleUpdate, isAuthorized],
|
[currentRepository, handleRemove, handleUpdate, isAuthorized],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||||
@@ -195,6 +207,7 @@ export default function PackageTable(): React.JSX.Element {
|
|||||||
<PackageRebuildDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "rebuild"} />
|
<PackageRebuildDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "rebuild"} />
|
||||||
<KeyImportDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "keyImport"} />
|
<KeyImportDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "keyImport"} />
|
||||||
<PackageInfoDialog
|
<PackageInfoDialog
|
||||||
|
key={`${currentRepository?.key}/${table.selectedPackage}`}
|
||||||
onClose={() => table.setSelectedPackage(null)}
|
onClose={() => table.setSelectedPackage(null)}
|
||||||
open={table.selectedPackage !== null}
|
open={table.selectedPackage !== null}
|
||||||
packageBase={table.selectedPackage}
|
packageBase={table.selectedPackage}
|
||||||
|
|||||||
@@ -18,31 +18,23 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { RepositoryContext } from "contexts/RepositoryContext";
|
import { RepositoryContext } from "contexts/RepositoryContext";
|
||||||
|
import { navigate, packageHref, useLocationQuery } from "hooks/useLocationQuery";
|
||||||
import type { RepositoryId } from "models/RepositoryId";
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
import React, { type ReactNode, useCallback, useMemo, useState, useSyncExternalStore } from "react";
|
import React, { type ReactNode, useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
function subscribeToHash(callback: () => void): () => void {
|
|
||||||
window.addEventListener("hashchange", callback);
|
|
||||||
return () => window.removeEventListener("hashchange", callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHashSnapshot(): string {
|
|
||||||
return window.location.hash.replace("#", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RepositoryProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
export function RepositoryProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
||||||
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
||||||
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
const repositoryKey = new URLSearchParams(useLocationQuery()).get("repository_id");
|
||||||
|
|
||||||
const currentRepository = useMemo(() => {
|
const currentRepository = useMemo(() => {
|
||||||
if (repositories.length === 0) {
|
if (repositories.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
return repositories.find(repository => repository.key === repositoryKey) ?? repositories[0] ?? null;
|
||||||
}, [repositories, hash]);
|
}, [repositories, repositoryKey]);
|
||||||
|
|
||||||
const setCurrentRepository = useCallback((repository: RepositoryId) => {
|
const setCurrentRepository = useCallback((repository: RepositoryId) => {
|
||||||
window.location.hash = repository.key;
|
navigate(packageHref(repository.key, null));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const value = useMemo(() => ({
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
|
||||||
|
function subscribe(callback: () => void): () => void {
|
||||||
|
window.addEventListener("popstate", callback);
|
||||||
|
return () => window.removeEventListener("popstate", callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSnapshot(): string {
|
||||||
|
return window.location.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLocationQuery(): string {
|
||||||
|
return useSyncExternalStore(subscribe, getSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function packageHref(repositoryKey: string, packageBase: string | null): string {
|
||||||
|
const query = new URLSearchParams(window.location.search);
|
||||||
|
query.set("repository_id", repositoryKey);
|
||||||
|
if (packageBase) {
|
||||||
|
query.set("package", packageBase);
|
||||||
|
} else {
|
||||||
|
query.delete("package");
|
||||||
|
}
|
||||||
|
return `${window.location.pathname}?${query}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function navigate(href: string): void {
|
||||||
|
if (href === `${window.location.pathname}${window.location.search}`) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.history.pushState(null, "", href);
|
||||||
|
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
*/
|
*/
|
||||||
import type { GridFilterModel } from "@mui/x-data-grid";
|
import type { GridFilterModel } from "@mui/x-data-grid";
|
||||||
import { useLocalStorage } from "hooks/useLocalStorage";
|
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||||
|
import { navigate, packageHref, useLocationQuery } from "hooks/useLocationQuery";
|
||||||
|
import { useRepository } from "hooks/useRepository";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export type DialogType = "dashboard" | "add" | "rebuild" | "keyImport";
|
export type DialogType = "dashboard" | "add" | "rebuild" | "keyImport";
|
||||||
@@ -43,7 +45,14 @@ export interface UseTableStateResult {
|
|||||||
export function useTableState(): UseTableStateResult {
|
export function useTableState(): UseTableStateResult {
|
||||||
const [selectionModel, setSelectionModel] = useState<string[]>([]);
|
const [selectionModel, setSelectionModel] = useState<string[]>([]);
|
||||||
const [dialogOpen, setDialogOpen] = useState<DialogType | null>(null);
|
const [dialogOpen, setDialogOpen] = useState<DialogType | null>(null);
|
||||||
const [selectedPackage, setSelectedPackage] = useState<string | null>(null);
|
const { currentRepository } = useRepository();
|
||||||
|
const packageBase = new URLSearchParams(useLocationQuery()).get("package");
|
||||||
|
const selectedPackage = currentRepository && packageBase ? packageBase : null;
|
||||||
|
const setSelectedPackage = (base: string | null): void => {
|
||||||
|
if (currentRepository) {
|
||||||
|
navigate(packageHref(currentRepository.key, base));
|
||||||
|
}
|
||||||
|
};
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
const [paginationModel, setPaginationModel] = useLocalStorage("ahriman-packages-pagination", {
|
const [paginationModel, setPaginationModel] = useLocalStorage("ahriman-packages-pagination", {
|
||||||
|
|||||||
Reference in new issue
Block a user