separate share and versions

This commit is contained in:
1ilit
2025-07-27 16:12:27 +04:00
parent 2b9b65b443
commit a5753fa365
7 changed files with 181 additions and 65 deletions

View File

@@ -1,11 +1,12 @@
import axios from "axios"; import axios from "axios";
const filename = "share.json"; export const SHARE_FILENAME = "share.json";
const description = "drawDB diagram"; export const VERSION_FILENAME = "versionned.json";
const description = "drawDB diagram";
const baseUrl = import.meta.env.VITE_BACKEND_URL; const baseUrl = import.meta.env.VITE_BACKEND_URL;
export async function create(content) { export async function create(filename, content) {
const res = await axios.post(`${baseUrl}/gists`, { const res = await axios.post(`${baseUrl}/gists`, {
public: false, public: false,
filename, filename,
@@ -16,7 +17,7 @@ export async function create(content) {
return res.data.data.id; return res.data.data.id;
} }
export async function patch(gistId, content) { export async function patch(gistId, filename, content) {
await axios.patch(`${baseUrl}/gists/${gistId}`, { await axios.patch(`${baseUrl}/gists/${gistId}`, {
filename, filename,
content, content,
@@ -49,3 +50,14 @@ export async function getVersion(gistId, sha) {
return res.data; return res.data;
} }
export async function getCommitsWithFile(gistId, file, perPage = 20, page = 1) {
const res = await axios.get(`${baseUrl}/gists/${gistId}/file-versions/${file}`, {
params: {
per_page: perPage,
page,
},
});
return res.data;
}

View File

@@ -1513,6 +1513,8 @@ export default function ControlPanel({
/> />
<Sidesheet <Sidesheet
type={sidesheet} type={sidesheet}
title={title}
setTitle={setTitle}
onClose={() => setSidesheet(SIDESHEET.NONE)} onClose={() => setSidesheet(SIDESHEET.NONE)}
/> />
</> </>

View File

@@ -13,7 +13,7 @@ import {
} from "../../../hooks"; } from "../../../hooks";
import { databases } from "../../../data/databases"; import { databases } from "../../../data/databases";
import { MODAL } from "../../../data/constants"; import { MODAL } from "../../../data/constants";
import { create, del, patch } from "../../../api/gists"; import { create, patch, SHARE_FILENAME } from "../../../api/gists";
export default function Share({ title, setModal }) { export default function Share({ title, setModal }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -55,24 +55,23 @@ export default function Share({ title, setModal }) {
const unshare = useCallback(async () => { const unshare = useCallback(async () => {
try { try {
await del(gistId); await patch(gistId, SHARE_FILENAME, undefined);
setGistId("");
setModal(MODAL.NONE); setModal(MODAL.NONE);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setError(e); setError(e);
} }
}, [gistId, setGistId, setModal]); }, [gistId, setModal]);
useEffect(() => { useEffect(() => {
const updateOrGenerateLink = async () => { const updateOrGenerateLink = async () => {
try { try {
setLoading(true); setLoading(true);
if (!gistId || gistId === "") { if (!gistId || gistId === "") {
const id = await create(diagramToString()); const id = await create(SHARE_FILENAME, diagramToString());
setGistId(id); setGistId(id);
} else { } else {
await patch(gistId, diagramToString()); await patch(gistId, SHARE_FILENAME, diagramToString());
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@@ -7,19 +7,63 @@ import {
IconChevronRight, IconChevronRight,
IconChevronLeft, IconChevronLeft,
} from "@douyinfe/semi-icons"; } from "@douyinfe/semi-icons";
import { getCommits, getVersion } from "../../../api/gists"; import {
create,
getCommitsWithFile,
getVersion,
patch,
VERSION_FILENAME,
} from "../../../api/gists";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { useAreas, useDiagram, useLayout } from "../../../hooks"; import {
useAreas,
useDiagram,
useEnums,
useLayout,
useNotes,
useTransform,
useTypes,
} from "../../../hooks";
import { databases } from "../../../data/databases";
export default function Revisions({ open }) { export default function Revisions({ open, title, setTitle }) {
const { gistId, setVersion } = useContext(IdContext); const { gistId, setVersion } = useContext(IdContext);
const { setAreas } = useAreas(); const { areas, setAreas } = useAreas();
const { setLayout } = useLayout(); const { setLayout } = useLayout();
const { setTables, setRelationships } = useDiagram(); const { database, tables, relationships, setTables, setRelationships } =
useDiagram();
const { notes, setNotes } = useNotes();
const { types, setTypes } = useTypes();
const { enums, setEnums } = useEnums();
const { transform } = useTransform();
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [revisions, setRevisions] = useState([]); const [revisions, setRevisions] = useState([]);
const diagramToString = useCallback(() => {
return JSON.stringify({
title,
tables,
relationships: relationships,
notes: notes,
subjectAreas: areas,
database: database,
...(databases[database].hasTypes && { types: types }),
...(databases[database].hasEnums && { enums: enums }),
transform: transform,
});
}, [
areas,
notes,
tables,
relationships,
database,
title,
enums,
types,
transform,
]);
const loadVersion = useCallback( const loadVersion = useCallback(
async (sha) => { async (sha) => {
try { try {
@@ -27,26 +71,49 @@ export default function Revisions({ open }) {
setVersion(sha); setVersion(sha);
setLayout((prev) => ({ ...prev, readOnly: true })); setLayout((prev) => ({ ...prev, readOnly: true }));
const content = version.data.files["share.json"].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);
setRelationships(parsedDiagram.relationships); setRelationships(parsedDiagram.relationships);
setAreas(parsedDiagram.subjectAreas); setAreas(parsedDiagram.subjectAreas);
setNotes(parsedDiagram.notes);
setTitle(parsedDiagram.title);
if (databases[database].hasTypes) {
setTypes(parsedDiagram.types);
}
if (databases[database].hasEnums) {
setEnums(parsedDiagram.enums);
}
} catch (e) { } catch (e) {
console.log(e); console.log(e);
Toast.error("failed_to_load_diagram"); Toast.error("failed_to_load_diagram");
} }
}, },
[gistId, setTables, setRelationships, setAreas, setVersion, setLayout], [
gistId,
setTables,
setRelationships,
setAreas,
setVersion,
setLayout,
database,
setNotes,
setTypes,
setEnums,
setTitle,
],
); );
useEffect(() => { const getRevisions = useCallback(
const getRevisions = async (gistId) => { async (gistId) => {
try { try {
setIsLoading(true); setIsLoading(true);
const { data } = await getCommits(gistId); const { data } = await getCommitsWithFile(gistId, VERSION_FILENAME);
setRevisions( setRevisions(
data.filter((version) => version.change_status.total !== 0), data.filter((version) => version.change_status.total !== 0),
); );
@@ -56,55 +123,72 @@ export default function Revisions({ open }) {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; },
[t],
);
const recordVersion = async () => {
try {
if (gistId) {
console.log(gistId)
await patch(gistId, VERSION_FILENAME, diagramToString());
} else {
await create(VERSION_FILENAME, diagramToString());
}
await getRevisions(gistId);
} catch (e) {
Toast.error("failed_to_record_version");
}
};
useEffect(() => {
if (gistId && open) { if (gistId && open) {
getRevisions(gistId); getRevisions(gistId);
} }
}, [gistId, t, open]); }, [gistId, getRevisions, open]);
if (gistId && isLoading) {
return (
<div className="text-blue-500 text-center">
<Spin size="middle" />
<div>{t("loading")}</div>
</div>
);
}
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 flex gap-2">
<IconButton icon={<IconChevronLeft />} title="Previous" /> <IconButton icon={<IconChevronLeft />} title="Previous" />
<Button icon={<IconPlus />} block onClick={() => {}}> <Button icon={<IconPlus />} block onClick={recordVersion}>
{t("record_version")} {t("record_version")}
</Button> </Button>
<IconButton icon={<IconChevronRight />} title="Next" /> <IconButton icon={<IconChevronRight />} title="Next" />
</div> </div>
{!gistId && <div className="my-3">{t("no_saved_revisions")}</div>} {isLoading ? (
{gistId && ( <div className="text-blue-500 text-center mt-3">
<div className="my-3 overflow-y-auto"> <Spin size="middle" />
<Steps direction="vertical" type="basic"> <div>{t("loading")}</div>
{revisions.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")} ${revisions.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>
) : (
<>
{!gistId && <div className="my-3">{t("no_saved_revisions")}</div>}
{gistId && (
<div className="my-3 overflow-y-auto">
<Steps direction="vertical" type="basic">
{revisions.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")} ${revisions.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

@@ -5,7 +5,7 @@ import Todo from "./Todo";
import Revisions from "./Revisions"; import Revisions from "./Revisions";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function Sidesheet({ type, onClose }) { export default function Sidesheet({ type, title, setTitle, onClose }) {
const { t } = useTranslation(); const { t } = useTranslation();
function getTitle(type) { function getTitle(type) {
@@ -28,7 +28,13 @@ export default function Sidesheet({ type, onClose }) {
case SIDESHEET.TODO: case SIDESHEET.TODO:
return <Todo />; return <Todo />;
case SIDESHEET.REVISIONS: case SIDESHEET.REVISIONS:
return <Revisions open={type !== SIDESHEET.NONE} />; return (
<Revisions
open={type !== SIDESHEET.NONE}
title={title}
setTitle={setTitle}
/>
);
default: default:
break; break;
} }

View File

@@ -25,7 +25,7 @@ import { useTranslation } from "react-i18next";
import { databases } from "../data/databases"; import { databases } from "../data/databases";
import { isRtl } from "../i18n/utils/rtl"; import { isRtl } from "../i18n/utils/rtl";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { get } from "../api/gists"; import { get, SHARE_FILENAME } from "../api/gists";
export const IdContext = createContext({ export const IdContext = createContext({
gistId: "", gistId: "",
@@ -293,7 +293,7 @@ export default function WorkSpace() {
const loadFromGist = async (shareId) => { const loadFromGist = async (shareId) => {
try { try {
const { data } = await get(shareId); const { data } = await get(shareId);
const parsedDiagram = JSON.parse(data.files["share.json"].content); const parsedDiagram = JSON.parse(data.files[SHARE_FILENAME].content);
setUndoStack([]); setUndoStack([]);
setRedoStack([]); setRedoStack([]);
setGistId(shareId); setGistId(shareId);
@@ -372,6 +372,12 @@ export default function WorkSpace() {
searchParams, searchParams,
]); ]);
const returnToCurrentDiagram = async () => {
await load();
setLayout((prev) => ({ ...prev, readOnly: false }));
setVersion(null);
};
useEffect(() => { useEffect(() => {
if ( if (
tables?.length === 0 && tables?.length === 0 &&
@@ -447,15 +453,20 @@ export default function WorkSpace() {
<Canvas saveState={saveState} setSaveState={setSaveState} /> <Canvas saveState={saveState} setSaveState={setSaveState} />
</CanvasContextProvider> </CanvasContextProvider>
{version && ( {version && (
<div <div className="absolute right-8 top-2 space-x-2">
className="absolute right-8 top-2"
onClick={() => setShowRestoreModal(true)}
>
<Button <Button
icon={<i className="fa-solid fa-rotate-right mt-0.5"></i>} icon={<i className="fa-solid fa-rotate-right mt-0.5"></i>}
onClick={() => setShowRestoreModal(true)}
> >
{t("restore_version")} {t("restore_version")}
</Button> </Button>
<Button
type="tertiary"
onClick={returnToCurrentDiagram}
icon={<i className="bi bi-arrow-return-right mt-1"></i>}
>
{t("return_to_current")}
</Button>
</div> </div>
)} )}
{!(layout.sidebar || layout.toolbar || layout.header) && ( {!(layout.sidebar || layout.toolbar || layout.header) && (
@@ -521,7 +532,8 @@ export default function WorkSpace() {
onCancel={() => setShowRestoreModal(false)} onCancel={() => setShowRestoreModal(false)}
title={ title={
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<IconAlertTriangle className="text-amber-400" size="extra-large" /> {t("restore_version")} <IconAlertTriangle className="text-amber-400" size="extra-large" />{" "}
{t("restore_version")}
</span> </span>
} }
okText={t("continue")} okText={t("continue")}

View File

@@ -267,7 +267,8 @@ 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 the current changes.",
return_to_current: "Return to diagram"
}, },
}; };