mirror of
https://github.com/drawdb-io/drawdb.git
synced 2025-09-01 10:25:13 +00:00
finalize versioning implementation, add pagination
This commit is contained in:
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user