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; 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( const res = await axios.get(
`${baseUrl}/gists/${gistId}/file-versions/${file}`, `${baseUrl}/gists/${gistId}/file-versions/${file}`,
{ {
params: { params: {
per_page: perPage, limit,
page, 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 { IdContext } from "../../Workspace";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, IconButton, Spin, Steps, Tag, Toast } from "@douyinfe/semi-ui"; import { Button, Spin, Steps, Tag, Toast } from "@douyinfe/semi-ui";
import { import { IconPlus } from "@douyinfe/semi-icons";
IconPlus,
IconChevronRight,
IconChevronLeft,
} from "@douyinfe/semi-icons";
import { import {
create, create,
getCommitsWithFile, getCommitsWithFile,
@@ -28,8 +24,24 @@ import {
} from "../../../hooks"; } from "../../../hooks";
import { databases } from "../../../data/databases"; 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 }) { 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 { areas, setAreas } = useAreas();
const { setLayout } = useLayout(); const { setLayout } = useLayout();
const { database, tables, relationships, setTables, setRelationships } = const { database, tables, relationships, setTables, setRelationships } =
@@ -41,6 +53,11 @@ export default function Versions({ open, title, setTitle }) {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [versions, setVersions] = useState([]); 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(() => { const diagramToString = useCallback(() => {
return JSON.stringify({ return JSON.stringify({
@@ -66,6 +83,11 @@ export default function Versions({ open, title, setTitle }) {
transform, transform,
]); ]);
const currentStep = useMemo(() => {
if (!version) return 0;
return versions.findIndex((v) => v.version === version);
}, [version, versions]);
const loadVersion = useCallback( const loadVersion = useCallback(
async (sha) => { async (sha) => {
try { try {
@@ -74,7 +96,6 @@ export default function Versions({ open, title, setTitle }) {
setLayout((prev) => ({ ...prev, readOnly: true })); setLayout((prev) => ({ ...prev, readOnly: true }));
const content = version.data.files[VERSION_FILENAME].content; const content = version.data.files[VERSION_FILENAME].content;
const parsedDiagram = JSON.parse(content); const parsedDiagram = JSON.parse(content);
setTables(parsedDiagram.tables); setTables(parsedDiagram.tables);
@@ -91,7 +112,6 @@ export default function Versions({ open, title, setTitle }) {
setEnums(parsedDiagram.enums); setEnums(parsedDiagram.enums);
} }
} catch (e) { } catch (e) {
console.log(e);
Toast.error("failed_to_load_diagram"); Toast.error("failed_to_load_diagram");
} }
}, },
@@ -111,24 +131,52 @@ export default function Versions({ open, title, setTitle }) {
); );
const getRevisions = useCallback( const getRevisions = useCallback(
async (gistId) => { async (cursorParam) => {
try { try {
if (!gistId) return;
setIsLoading(true); setIsLoading(true);
const { data } = await getCommitsWithFile(gistId, VERSION_FILENAME);
setVersions( const cached = cacheRef[gistId];
data.filter((version) => version.change_status.total !== 0), 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) { } catch (e) {
console.log(e);
Toast.error(t("oops_smth_went_wrong")); Toast.error(t("oops_smth_went_wrong"));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, },
[t], [gistId, versions, t, cacheRef],
); );
const hasDiagramChanged = async () => { const hasDiagramChanged = async () => {
if (!gistId) return true;
const previousVersion = await get(gistId); const previousVersion = await get(gistId);
const previousDiagram = JSON.parse( const previousDiagram = JSON.parse(
previousVersion.data.files[VERSION_FILENAME]?.content, previousVersion.data.files[VERSION_FILENAME]?.content,
@@ -150,6 +198,7 @@ export default function Versions({ open, title, setTitle }) {
const recordVersion = async () => { const recordVersion = async () => {
try { try {
setIsRecording(true);
const hasChanges = await hasDiagramChanged(); const hasChanges = await hasDiagramChanged();
if (!hasChanges) { if (!hasChanges) {
Toast.info(t("no_changes_to_record")); Toast.info(t("no_changes_to_record"));
@@ -160,63 +209,92 @@ export default function Versions({ open, title, setTitle }) {
} else { } else {
const id = await create(VERSION_FILENAME, diagramToString()); const id = await create(VERSION_FILENAME, diagramToString());
setGistId(id); setGistId(id);
console.log("new gist created", id);
} }
await getRevisions(gistId);
delete cacheRef[gistId];
saveCache(cacheRef);
await getRevisions();
} catch (e) { } catch (e) {
Toast.error("failed_to_record_version"); Toast.error("failed_to_record_version");
} finally {
setIsRecording(false);
} }
}; };
const onClearCache = () => {
delete cacheRef[gistId];
saveCache(cacheRef);
Toast.success(t("cache_cleared"));
};
useEffect(() => { useEffect(() => {
if (gistId && open) { if (gistId && open) {
getRevisions(gistId); getRevisions();
} }
}, [gistId, getRevisions, open]); }, [gistId, open, getRevisions]);
return ( return (
<div className="mx-5 relative h-full"> <div className="mx-5 relative h-full">
<div className="sticky top-0 z-10 sidesheet-theme pb-2 flex gap-2"> <div className="sticky top-0 z-10 sidesheet-theme pb-2 grid grid-cols-3 gap-2">
<IconButton icon={<IconChevronLeft />} title="Previous" /> <Button
<Button icon={<IconPlus />} block onClick={recordVersion}> className={cacheRef[gistId] ? "col-span-2" : "col-span-3"}
block
icon={isRecording ? <Spin /> : <IconPlus />}
disabled={isLoading || isRecording}
onClick={recordVersion}
>
{t("record_version")} {t("record_version")}
</Button> </Button>
<IconButton icon={<IconChevronRight />} title="Next" />
{cacheRef[gistId] && (
<Button block type="danger" onClick={onClearCache}>
{t("clear_cache")}
</Button>
)}
</div> </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" /> <Spin size="middle" />
<div>{t("loading")}</div> <div>{t("loading")}</div>
</div> </div>
) : ( )}
<> {hasMore && !isLoading && (
{(!gistId || !versions.length) && ( <div className="text-center">
<div className="my-3">{t("no_saved_versions")}</div> <Button onClick={() => getRevisions(cursor)}>{t("load_more")}</Button>
)} </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>
)}
</>
)} )}
</div> </div>
); );

View File

@@ -267,9 +267,13 @@ const en = {
read_only: "Read only", read_only: "Read only",
continue: "Continue", continue: "Continue",
restore_version: "Restore version", 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", return_to_current: "Return to diagram",
no_changes_to_record: "No changes to record", 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",
}, },
}; };