Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#9433: use PaginatedTable for Mod Version History #9527

Merged
merged 17 commits into from
Nov 15, 2024
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
1 change: 1 addition & 0 deletions applications/browser-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"simple-icons": "^5.8.0",
"slugify": "^1.6.6",
"stemmer": "^2.0.1",
"thenby": "^1.3.4",
"uint8array-extras": "^1.4.0",
"use-async-effect": "^2.2.7",
"use-debounce": "^10.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ interface TableProps<
actions?: Actions;
initialPageSize?: number;
rowProps?: (row: UnknownObject) => RowProps;
showSearchFilter: boolean;
showSearchFilter?: boolean;

/**
* Force the table to show a specific record. This can be a function that returns true for the record to show, or
Expand All @@ -57,6 +57,11 @@ interface TableProps<
* tables mounted, this will cause an infinite history loop.
*/
syncURL?: boolean;

/**
* Message to display when there are no records to show.
*/
emptyMessage?: string;
}

const SearchFilter: React.FunctionComponent<{
Expand Down Expand Up @@ -143,7 +148,8 @@ function PaginatedTable<
initialPageSize = 10,
syncURL = false,
rowProps,
showSearchFilter,
emptyMessage = "No records found.",
showSearchFilter = true,
forceShowRecord,
}: TableProps<Row, Actions>): React.ReactElement {
const history = useHistory();
Expand Down Expand Up @@ -299,7 +305,7 @@ function PaginatedTable<
{rows.length === 0 && (
<tr>
<td colSpan={5} className="text-muted">
No records found.
{emptyMessage}
</td>
</tr>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ describe("ModVersionHistory", () => {

await waitFor(() => {
expect(
screen.getByText("Version History requires mod write permission"),
screen.getByText(
"Viewing Version History requires mod write permission",
),
).toBeInTheDocument();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import React, { useMemo } from "react";
import { useSelector } from "react-redux";
import { selectActiveModId } from "@/pageEditor/store/editor/editorSelectors";
import { assertNotNullish } from "@/utils/nullishUtils";
import { Card, Container, Table } from "react-bootstrap";
import { Card, Container } from "react-bootstrap";
import styles from "@/pageEditor/tabs/modVariablesDefinition/ModVariablesDefinitionEditor.module.scss";
import ErrorBoundary from "@/components/ErrorBoundary";
import {
Expand All @@ -33,10 +33,64 @@ import AsyncStateGate from "@/components/AsyncStateGate";
import { dateFormat } from "@/utils/stringUtils";
import { mergeAsyncState, valueToAsyncState } from "@/utils/asyncStateUtils";
import { isInternalRegistryId } from "@/utils/registryUtils";
import type { Column, Row } from "react-table";
import PaginatedTable from "@/components/paginatedTable/PaginatedTable";
import { MemoryRouter } from "react-router";
import { firstBy } from "thenby";
import { compare } from "semver";

function useModPackageVersionsQuery(
modId: RegistryId,
): AsyncState<PackageVersionDeprecated[] | string> {
type TableColumn = Column<PackageVersionDeprecated>;

const COLUMNS: TableColumn[] = [
{
Header: "Version",
accessor: "version",
sortDescFirst: true,
sortType(rowA, rowB, columnId) {
return compare(rowA.original.version, rowB.original.version);
},
},
{
Header: "Timestamp",
accessor: "updated_at",
sortDescFirst: true,
Cell({ value }) {
return <>{dateFormat.format(Date.parse(value))}</>;
},
},
{
Header: "Updated By",
accessor: "updated_by",
sortType: firstBy(
(x: Row<PackageVersionDeprecated>) => x.original.updated_by?.email ?? "",
),
Cell({ value }) {
const { email } = value ?? {};
return email ? (
<a href={`mailto:${email}`}>{email}</a>
) : (
<span className="text-muted">Unknown</span>
);
},
},
{
Header: "Message",
accessor: "message",
disableSortBy: true,
Cell({ value }) {
return value ? (
<>{value}</>
) : (
<span className="text-muted">No message provided</span>
);
},
},
];

function useModPackageVersionsQuery(modId: RegistryId): AsyncState<{
data: PackageVersionDeprecated[];
message: string | undefined;
}> {
// Lookup the surrogate key for the package
const editablePackagesQuery = useGetEditablePackagesQuery(undefined, {
skip: isInternalRegistryId(modId),
Expand All @@ -57,46 +111,30 @@ function useModPackageVersionsQuery(

return useMemo(() => {
if (isInternalRegistryId(modId)) {
return valueToAsyncState("Version History unavailable for unsaved mods");
return valueToAsyncState({
data: [],
message: "Version History unavailable for unsaved mods",
});
}

if (editablePackage) {
return packageVersionsQuery;
return mergeAsyncState(
packageVersionsQuery,
(data: PackageVersionDeprecated[]) => ({
data,
message: undefined,
}),
);
}

return mergeAsyncState(
editablePackagesQuery,
() => "Version History requires mod write permission",
);
return mergeAsyncState(editablePackagesQuery, () => ({
data: [],
message: "Viewing Version History requires mod write permission",
}));
}, [modId, editablePackage, packageVersionsQuery, editablePackagesQuery]);
}

const PackageVersionRow: React.VFC<{ version: PackageVersionDeprecated }> = ({
version,
}) => {
const email = version.updated_by?.email;

return (
<tr>
<td>{version.version}</td>
<td>{dateFormat.format(Date.parse(version.updated_at))}</td>
<td>
{email ? (
<a href={`mailto:${email}`}>{email}</a>
) : (
<span className="text-muted">Unknown</span>
)}
</td>
<td>
{version.message ?? (
<span className="text-muted">No message provided</span>
)}
</td>
</tr>
);
};

const ModVersionHistory: React.FC = () => {
const ModVersionHistory: React.VFC = () => {
const modId = useSelector(selectActiveModId);

assertNotNullish(modId, "No active mod id");
Expand All @@ -108,34 +146,23 @@ const ModVersionHistory: React.FC = () => {
<ErrorBoundary>
<Card>
<Card.Header>Version History</Card.Header>
<Card.Body>
<AsyncStateGate state={packageVersionsQuery}>
{({ data }) => (
<Table>
<thead>
<tr>
<th>Version</th>
<th>Timestamp</th>
<th>Updated By</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{typeof data === "object" ? (
data.map((version) => (
<PackageVersionRow key={version.id} version={version} />
))
) : (
<tr>
<td colSpan={4} className="text-muted">
{data}
</td>
</tr>
)}
</tbody>
</Table>
)}
</AsyncStateGate>
<Card.Body className="p-0">
<MemoryRouter>
<AsyncStateGate state={packageVersionsQuery}>
{({ data: { data, message } }) => (
// PaginatedTable includes a useLocation call because it supports syncing the page number with the URL
// We're not using that feature here, but still need to ensure it's wrapped in a Router so the
// useLocation call doesn't error
<MemoryRouter>
<PaginatedTable
columns={COLUMNS}
data={data}
emptyMessage={message}
/>
</MemoryRouter>
)}
</AsyncStateGate>
</MemoryRouter>
</Card.Body>
</Card>
</ErrorBoundary>
Expand Down
2 changes: 1 addition & 1 deletion applications/browser-extension/src/types/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export type Database = components["schemas"]["Database"];
// TODO remove in https://github.com/pixiebrix/pixiebrix-extension/issues/7692
export type PackageVersionDeprecated = SetRequired<
components["schemas"]["PackageVersionDeprecated"],
"updated_at" | "created_at" | "id"
"updated_at" | "created_at" | "id" | "version"
>;

export type PendingInvitation = components["schemas"]["PendingInvitation"];
Expand Down
1 change: 1 addition & 0 deletions knip.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ const knipConfig = {
"@tiptap/extension-underline",
"@tiptap/extension-link",
"@tiptap/extension-image",
"thenby",

// False positives flagged in --production checks.
// In non-production runs, these entries are flagged as unnecessary ignoreDependencies entries
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading