mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-29 02:18:09 +03:00
feat: uncommitted changes UI
This commit is contained in:
@@ -4,14 +4,14 @@ import useSetSubheader from "components/layout/header/useSetSubheader";
|
||||
import Loading from "components/util/Loading";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
import Select from "components/util/Select";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import { useParams } from "react-router-dom";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Col, Modal, Row } from "react-bootstrap";
|
||||
import Divider from "components/util/Divider";
|
||||
import Icon from "components/util/Icon";
|
||||
import { SetState } from "types/react";
|
||||
import { TachiConfig } from "lib/config";
|
||||
import ExternalLink from "components/util/ExternalLink";
|
||||
import Card from "components/layout/page/Card";
|
||||
|
||||
export default function SeedsViewer() {
|
||||
useSetSubheader(["Developer Utils", "Database Seeds Management"]);
|
||||
@@ -58,54 +58,174 @@ export default function SeedsViewer() {
|
||||
// return <BMSCourseLookupTable dataset={} />;
|
||||
}
|
||||
|
||||
type Revision = { c: GitCommit; repo: string };
|
||||
|
||||
function InnerSeedsViewer({ hasLocalAPI }: { hasLocalAPI: boolean }) {
|
||||
const { repo: initRepo } = useParams<{
|
||||
repo: string | undefined;
|
||||
}>();
|
||||
// base, rev a-la traditional git comparisons. Head is expected to be 'after'
|
||||
// the base, but no order is enforced.
|
||||
const [baseRev, setBaseRev] = useState<Revision | null>(null);
|
||||
const [headRev, setHeadRev] = useState<Revision | null>(null);
|
||||
|
||||
// a repo is one of the following:
|
||||
// null - nothing has been selected yet
|
||||
// "local" - we're referring to the files on the local development disk
|
||||
// "gh:NAME/REPO" - we're referring to a repository on github, like gh:TNG-Dev/Tachi
|
||||
const [repo, setRepo] = useState(initRepo ?? null);
|
||||
|
||||
// A revision is any valid pointer to a commit that Github understands.
|
||||
const [rev, setRev] = useState<GitCommit | null>(null);
|
||||
// if we have a base and a head, render the two separately.
|
||||
if (baseRev && headRev) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col xs={12} lg={6} className="offset-lg-3 text-center">
|
||||
<div>
|
||||
<span style={{ fontSize: "large" }}>Repository:</span>
|
||||
<Select
|
||||
style={{ display: "inline", width: "unset" }}
|
||||
className="mx-2"
|
||||
value={repo}
|
||||
setValue={setRepo}
|
||||
allowNull
|
||||
>
|
||||
{hasLocalAPI && <option value="local">Your Local Repo</option>}
|
||||
</Select>
|
||||
{rev && (
|
||||
<>
|
||||
<span>/</span>
|
||||
{rev.sha}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={12} lg={10} className="offset-lg-1 text-center">
|
||||
{repo && !rev && (
|
||||
<>
|
||||
<Divider />
|
||||
<RevSelector repo={repo} setRev={setRev} />
|
||||
</>
|
||||
)}
|
||||
<Col xs={12} lg={baseRev ? 6 : 12} className="text-center">
|
||||
<SeedsPicker
|
||||
hasLocalAPI={hasLocalAPI}
|
||||
header="Base Commit"
|
||||
setRev={setBaseRev}
|
||||
rev={baseRev}
|
||||
/>
|
||||
</Col>
|
||||
{baseRev && (
|
||||
<Col xs={12} lg={6} className="text-center">
|
||||
<SeedsPicker
|
||||
hasLocalAPI={hasLocalAPI}
|
||||
header="Compare Commit"
|
||||
setRev={setHeadRev}
|
||||
rev={headRev}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function SeedsPicker({
|
||||
hasLocalAPI,
|
||||
header,
|
||||
rev,
|
||||
setRev,
|
||||
}: {
|
||||
hasLocalAPI: boolean;
|
||||
header: string;
|
||||
rev: Revision | null;
|
||||
setRev: SetState<Revision | null>;
|
||||
}) {
|
||||
// a repo is one of the following:
|
||||
// null - nothing has been selected yet
|
||||
// "local" - we're referring to the files on the local development disk
|
||||
// "GitHub:NAME/REPO" - we're referring to a repository on github, like GitHub:TNG-Dev/Tachi
|
||||
const [repo, setRepo] = useState<string | null>(null);
|
||||
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const [subject, body] = useMemo(() => {
|
||||
if (rev === null) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
const [subject, _gap, ...maybeBodies] = rev.c.commit.message.split("\n");
|
||||
|
||||
return [subject, maybeBodies?.join("\n") ?? null];
|
||||
}, [rev]);
|
||||
|
||||
const [showBody, setShowBody] = useState(false);
|
||||
|
||||
const authorNotCommitter = rev?.c.commit.author.email !== rev?.c.commit.committer.email;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card header={header}>
|
||||
{rev ? (
|
||||
<div className="mr-3" style={{ textAlign: "left" }}>
|
||||
<span style={{ fontSize: "1.15rem" }}>
|
||||
<code>
|
||||
{rev.repo}/{rev.c.sha}
|
||||
</code>
|
||||
: {subject}
|
||||
</span>
|
||||
{body && (
|
||||
<span
|
||||
className="ml-4 badge badge-secondary"
|
||||
onClick={() => setShowBody(!showBody)}
|
||||
>
|
||||
{showBody ? (
|
||||
<Icon style={{ fontSize: "0.7rem" }} type="chevron-down" />
|
||||
) : (
|
||||
<Icon style={{ fontSize: "0.7rem" }} type="chevron-left" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{showBody && <div className="ml-6 mt-1">{body}</div>}
|
||||
<Divider />
|
||||
<div style={{ width: "100%", display: "flex" }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
{authorNotCommitter ? (
|
||||
<>
|
||||
Authored by <b>{rev.c.commit.author.name}</b>, Committed by{" "}
|
||||
<b>{rev.c.commit.committer.name}</b>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Authored by <b>{rev.c.commit.author.name}</b>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right" style={{ flex: 1 }}>
|
||||
{FormatTime(Date.parse(rev.c.commit.author.date))}
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="text-center">
|
||||
<span
|
||||
className="text-muted underline-on-hover"
|
||||
onClick={() => setShow(true)}
|
||||
>
|
||||
Change Commit...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button variant="secondary" onClick={() => setShow(true)}>
|
||||
Pick Commit...
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
<Modal show={show} size="xl" onHide={() => setShow(false)}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Pick {header}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Row>
|
||||
<Col xs={12} className="text-center">
|
||||
<div>
|
||||
<span style={{ fontSize: "large" }}>Repository:</span>
|
||||
<Select
|
||||
style={{ display: "inline", width: "unset" }}
|
||||
className="mx-2"
|
||||
value={repo}
|
||||
setValue={setRepo}
|
||||
allowNull
|
||||
>
|
||||
{hasLocalAPI && <option value="local">Your Local Repo</option>}
|
||||
</Select>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={12} lg={10} className="offset-lg-1 text-center">
|
||||
{repo && (
|
||||
<>
|
||||
<Divider />
|
||||
<RevSelector
|
||||
repo={repo}
|
||||
onSelect={(commit) => {
|
||||
setRev({ c: commit, repo });
|
||||
setShow(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// stolen straight from server/src/utils/git.ts
|
||||
interface GitCommit {
|
||||
sha: string;
|
||||
@@ -124,12 +244,12 @@ interface GitCommit {
|
||||
};
|
||||
}
|
||||
|
||||
function RevSelector({ repo, setRev }: { repo: string; setRev: SetState<GitCommit | null> }) {
|
||||
function RevSelector({ repo, onSelect }: { repo: string; onSelect: (g: GitCommit) => void }) {
|
||||
const [revs, setRevs] = useState<Array<GitCommit>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (repo.startsWith("gh:")) {
|
||||
if (repo.startsWith("GitHub:")) {
|
||||
throw new Error("Unsupported...");
|
||||
} else {
|
||||
// local
|
||||
@@ -139,7 +259,32 @@ function RevSelector({ repo, setRev }: { repo: string; setRev: SetState<GitCommi
|
||||
throw new Error(`Failed to fetch commits? ${res.description}`);
|
||||
}
|
||||
|
||||
setRevs(res.body);
|
||||
const hasUncommittedRes = await APIFetchV1<boolean>(
|
||||
"/seeds/has-uncommitted-changes"
|
||||
);
|
||||
|
||||
if (hasUncommittedRes.success && hasUncommittedRes.body) {
|
||||
const LOCAL_COMMIT: GitCommit = {
|
||||
sha: "WORKING_DIRECTORY",
|
||||
commit: {
|
||||
author: {
|
||||
name: "Not Committed Yet",
|
||||
date: "1970-01-01",
|
||||
email: "null@example.com",
|
||||
},
|
||||
committer: {
|
||||
name: "Not Committed Yet",
|
||||
date: "1970-01-01",
|
||||
email: "null@example.com",
|
||||
},
|
||||
message: "Uncommitted changes on your local disk.",
|
||||
},
|
||||
};
|
||||
|
||||
setRevs([LOCAL_COMMIT, ...res.body]);
|
||||
} else {
|
||||
setRevs(res.body);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [repo]);
|
||||
@@ -148,13 +293,19 @@ function RevSelector({ repo, setRev }: { repo: string; setRev: SetState<GitCommi
|
||||
<div className="timeline timeline-2">
|
||||
<div className="timeline-bar"></div>
|
||||
{revs.map((r) => (
|
||||
<Revision key={r.sha} rev={r} setRev={setRev} />
|
||||
<Revision key={r.sha} rev={r} onSelect={onSelect} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Revision({ rev, setRev }: { rev: GitCommit; setRev: SetState<GitCommit | null> }) {
|
||||
function Revision({
|
||||
rev,
|
||||
onSelect: onSelect,
|
||||
}: {
|
||||
rev: GitCommit;
|
||||
onSelect: (g: GitCommit) => void;
|
||||
}) {
|
||||
const authorNotCommitter = rev.commit.author.email !== rev.commit.committer.email;
|
||||
|
||||
// if there's a body then there's two newlines.
|
||||
@@ -168,10 +319,10 @@ function Revision({ rev, setRev }: { rev: GitCommit; setRev: SetState<GitCommit
|
||||
<div className="timeline-item">
|
||||
<span className="timeline-badge bg-primary"></span>
|
||||
<div className="timeline-content d-flex align-items-center justify-content-between">
|
||||
<div className="mr-3" style={{ width: "50%", textAlign: "left" }}>
|
||||
<div className="mr-3" style={{ width: "70%", textAlign: "left" }}>
|
||||
<span style={{ fontSize: "1.15rem" }}>
|
||||
<code>{rev.sha}</code>:{" "}
|
||||
<a className="gentle-link" onClick={() => setRev(rev)}>
|
||||
<a className="gentle-link" onClick={() => onSelect(rev)}>
|
||||
{subject}
|
||||
</a>
|
||||
</span>
|
||||
|
||||
@@ -82,6 +82,10 @@ router.get("/has-uncommitted-changes", async (req, res) => {
|
||||
// local changes.
|
||||
const hasUncommittedChanges = stdout
|
||||
.split("\n")
|
||||
|
||||
// note that doing this properly is frustrating. This has false positives for
|
||||
// routes that partially contain this route. I've ameliorated this slightly with
|
||||
// a leading space, but that is not a proper solution.
|
||||
.some((row) => / database-seeds\/collections/u.exec(row));
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
Reference in New Issue
Block a user