finalize versioning implementation, add pagination

This commit is contained in:
1ilit
2025-08-24 18:38:23 +04:00
parent 97ab30a308
commit 6bf3317fae
3 changed files with 146 additions and 59 deletions

View File

@@ -53,13 +53,18 @@ export async function getVersion(gistId, sha) {
return res.data;
}
export async function getCommitsWithFile(gistId, file, perPage = 20, page = 1) {
export async function getCommitsWithFile(
gistId,
file,
limit = 10,
cursor = null,
) {
const res = await axios.get(
`${baseUrl}/gists/${gistId}/file-versions/${file}`,
{
params: {
per_page: perPage,
page,
limit,
cursor,
},
},
);

View File

@@ -1,12 +1,8 @@
import { useCallback, useContext, useEffect, useState } from "react";
import { useCallback, useContext, useEffect, useState, useMemo } from "react";
import { IdContext } from "../../Workspace";
import { useTranslation } from "react-i18next";
import { Button, IconButton, Spin, Steps, Tag, Toast } from "@douyinfe/semi-ui";
import {
IconPlus,
IconChevronRight,
IconChevronLeft,
} from "@douyinfe/semi-icons";
import { Button, Spin, Steps, Tag, Toast } from "@douyinfe/semi-ui";
import { IconPlus } from "@douyinfe/semi-icons";
import {
create,
getCommitsWithFile,
@@ -28,8 +24,24 @@ import {
} from "../../../hooks";
import { databases } from "../../../data/databases";
const LIMIT = 10;
const STORAGE_KEY = "versions_cache";
function loadCache() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : {};
} catch {
return {};
}
}
function saveCache(cache) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(cache));
}
export default function Versions({ open, title, setTitle }) {
const { gistId, setGistId, setVersion } = useContext(IdContext);
const { gistId, setGistId, version, setVersion } = useContext(IdContext);
const { areas, setAreas } = useAreas();
const { setLayout } = useLayout();
const { database, tables, relationships, setTables, setRelationships } =
@@ -41,6 +53,11 @@ export default function Versions({ open, title, setTitle }) {
const { t, i18n } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [versions, setVersions] = useState([]);
const [hasMore, setHasMore] = useState(false);
const [cursor, setCursor] = useState(null);
const [isRecording, setIsRecording] = useState(false);
const cacheRef = useMemo(() => loadCache(), []);
const diagramToString = useCallback(() => {
return JSON.stringify({
@@ -66,6 +83,11 @@ export default function Versions({ open, title, setTitle }) {
transform,
]);
const currentStep = useMemo(() => {
if (!version) return 0;
return versions.findIndex((v) => v.version === version);
}, [version, versions]);
const loadVersion = useCallback(
async (sha) => {
try {
@@ -74,7 +96,6 @@ export default function Versions({ open, title, setTitle }) {
setLayout((prev) => ({ ...prev, readOnly: true }));
const content = version.data.files[VERSION_FILENAME].content;
const parsedDiagram = JSON.parse(content);
setTables(parsedDiagram.tables);
@@ -91,7 +112,6 @@ export default function Versions({ open, title, setTitle }) {
setEnums(parsedDiagram.enums);
}
} catch (e) {
console.log(e);
Toast.error("failed_to_load_diagram");
}
},
@@ -111,24 +131,52 @@ export default function Versions({ open, title, setTitle }) {
);
const getRevisions = useCallback(
async (gistId) => {
async (cursorParam) => {
try {
if (!gistId) return;
setIsLoading(true);
const { data } = await getCommitsWithFile(gistId, VERSION_FILENAME);
setVersions(
data.filter((version) => version.change_status.total !== 0),
const cached = cacheRef[gistId];
if (cached && !cursorParam) {
setVersions(cached.versions);
setCursor(cached.cursor);
setHasMore(cached.hasMore);
setIsLoading(false);
return;
}
const res = await getCommitsWithFile(
gistId,
VERSION_FILENAME,
LIMIT,
cursorParam,
);
const newVersions = cursorParam ? [...versions, ...res.data] : res.data;
setVersions(newVersions);
setHasMore(res.pagination.hasMore);
setCursor(res.pagination.cursor);
cacheRef[gistId] = {
versions: newVersions,
cursor: res.pagination.cursor,
hasMore: res.pagination.hasMore,
};
saveCache(cacheRef);
} catch (e) {
console.log(e);
Toast.error(t("oops_smth_went_wrong"));
} finally {
setIsLoading(false);
}
},
[t],
[gistId, versions, t, cacheRef],
);
const hasDiagramChanged = async () => {
if (!gistId) return true;
const previousVersion = await get(gistId);
const previousDiagram = JSON.parse(
previousVersion.data.files[VERSION_FILENAME]?.content,
@@ -150,6 +198,7 @@ export default function Versions({ open, title, setTitle }) {
const recordVersion = async () => {
try {
setIsRecording(true);
const hasChanges = await hasDiagramChanged();
if (!hasChanges) {
Toast.info(t("no_changes_to_record"));
@@ -160,63 +209,92 @@ export default function Versions({ open, title, setTitle }) {
} else {
const id = await create(VERSION_FILENAME, diagramToString());
setGistId(id);
console.log("new gist created", id);
}
await getRevisions(gistId);
delete cacheRef[gistId];
saveCache(cacheRef);
await getRevisions();
} catch (e) {
Toast.error("failed_to_record_version");
} finally {
setIsRecording(false);
}
};
const onClearCache = () => {
delete cacheRef[gistId];
saveCache(cacheRef);
Toast.success(t("cache_cleared"));
};
useEffect(() => {
if (gistId && open) {
getRevisions(gistId);
getRevisions();
}
}, [gistId, getRevisions, open]);
}, [gistId, open, getRevisions]);
return (
<div className="mx-5 relative h-full">
<div className="sticky top-0 z-10 sidesheet-theme pb-2 flex gap-2">
<IconButton icon={<IconChevronLeft />} title="Previous" />
<Button icon={<IconPlus />} block onClick={recordVersion}>
<div className="sticky top-0 z-10 sidesheet-theme pb-2 grid grid-cols-3 gap-2">
<Button
className={cacheRef[gistId] ? "col-span-2" : "col-span-3"}
block
icon={isRecording ? <Spin /> : <IconPlus />}
disabled={isLoading || isRecording}
onClick={recordVersion}
>
{t("record_version")}
</Button>
<IconButton icon={<IconChevronRight />} title="Next" />
{cacheRef[gistId] && (
<Button block type="danger" onClick={onClearCache}>
{t("clear_cache")}
</Button>
)}
</div>
{isLoading ? (
<div className="text-blue-500 text-center mt-3">
{(!gistId || !versions.length) && !isLoading && (
<div className="my-3">{t("no_saved_versions")}</div>
)}
{gistId && (
<div className="my-3 overflow-y-auto">
<Steps direction="vertical" type="basic" current={currentStep}>
{versions.map((r) => (
<Steps.Step
key={r.version}
onClick={() => loadVersion(r.version)}
className="group"
title={
<div className="flex justify-between items-center w-full">
<Tag>{r.version.substring(0, 7)}</Tag>
<span className="text-xs hidden group-hover:inline-block">
{t("click_to_view")}
</span>
</div>
}
description={`${t("commited_at")} ${DateTime.fromISO(
r.committed_at,
)
.setLocale(i18n.language)
.toLocaleString(DateTime.DATETIME_MED)}`}
icon={<i className="text-sm fa-solid fa-asterisk" />}
/>
))}
</Steps>
</div>
)}
{isLoading && !isRecording && (
<div className="text-blue-500 text-center my-3">
<Spin size="middle" />
<div>{t("loading")}</div>
</div>
) : (
<>
{(!gistId || !versions.length) && (
<div className="my-3">{t("no_saved_versions")}</div>
)}
{gistId && (
<div className="my-3 overflow-y-auto">
<Steps direction="vertical" type="basic">
{versions.map((r, i) => (
<Steps.Step
key={r.version}
onClick={() => loadVersion(r.version)}
title={
<div className="flex justify-between items-center w-full">
<span>{`${t("version")} ${versions.length - i}`}</span>
<Tag>{r.version.substring(0, 7)}</Tag>
</div>
}
description={`${t("commited_at")} ${DateTime.fromISO(
r.committed_at,
)
.setLocale(i18n.language)
.toLocaleString(DateTime.DATETIME_MED)}`}
icon={<i className="text-sm fa-solid fa-asterisk" />}
/>
))}
</Steps>
</div>
)}
</>
)}
{hasMore && !isLoading && (
<div className="text-center">
<Button onClick={() => getRevisions(cursor)}>{t("load_more")}</Button>
</div>
)}
</div>
);

View File

@@ -267,9 +267,13 @@ const en = {
read_only: "Read only",
continue: "Continue",
restore_version: "Restore version",
restore_warning: "Loading another version will overwrite the current changes.",
restore_warning: "Loading another version will overwrite any changes.",
return_to_current: "Return to diagram",
no_changes_to_record: "No changes to record",
click_to_view: "Click to view",
load_more: "Load more",
clear_cache: "Clear cache",
cache_cleared: "Cache cleared",
},
};