mirror of
https://github.com/drawdb-io/drawdb.git
synced 2025-08-29 02:25:26 +00:00
separate share and versions
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import axios from "axios";
|
||||
|
||||
const filename = "share.json";
|
||||
const description = "drawDB diagram";
|
||||
export const SHARE_FILENAME = "share.json";
|
||||
export const VERSION_FILENAME = "versionned.json";
|
||||
|
||||
const description = "drawDB diagram";
|
||||
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`, {
|
||||
public: false,
|
||||
filename,
|
||||
@@ -16,7 +17,7 @@ export async function create(content) {
|
||||
return res.data.data.id;
|
||||
}
|
||||
|
||||
export async function patch(gistId, content) {
|
||||
export async function patch(gistId, filename, content) {
|
||||
await axios.patch(`${baseUrl}/gists/${gistId}`, {
|
||||
filename,
|
||||
content,
|
||||
@@ -49,3 +50,14 @@ export async function getVersion(gistId, sha) {
|
||||
|
||||
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;
|
||||
}
|
@@ -1513,6 +1513,8 @@ export default function ControlPanel({
|
||||
/>
|
||||
<Sidesheet
|
||||
type={sidesheet}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
onClose={() => setSidesheet(SIDESHEET.NONE)}
|
||||
/>
|
||||
</>
|
||||
|
@@ -13,7 +13,7 @@ import {
|
||||
} from "../../../hooks";
|
||||
import { databases } from "../../../data/databases";
|
||||
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 }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -55,24 +55,23 @@ export default function Share({ title, setModal }) {
|
||||
|
||||
const unshare = useCallback(async () => {
|
||||
try {
|
||||
await del(gistId);
|
||||
setGistId("");
|
||||
await patch(gistId, SHARE_FILENAME, undefined);
|
||||
setModal(MODAL.NONE);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(e);
|
||||
}
|
||||
}, [gistId, setGistId, setModal]);
|
||||
}, [gistId, setModal]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateOrGenerateLink = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (!gistId || gistId === "") {
|
||||
const id = await create(diagramToString());
|
||||
const id = await create(SHARE_FILENAME, diagramToString());
|
||||
setGistId(id);
|
||||
} else {
|
||||
await patch(gistId, diagramToString());
|
||||
await patch(gistId, SHARE_FILENAME, diagramToString());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
@@ -7,19 +7,63 @@ import {
|
||||
IconChevronRight,
|
||||
IconChevronLeft,
|
||||
} 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 { 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 { setAreas } = useAreas();
|
||||
const { areas, setAreas } = useAreas();
|
||||
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 [isLoading, setIsLoading] = useState(false);
|
||||
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(
|
||||
async (sha) => {
|
||||
try {
|
||||
@@ -27,26 +71,49 @@ export default function Revisions({ open }) {
|
||||
setVersion(sha);
|
||||
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);
|
||||
|
||||
setTables(parsedDiagram.tables);
|
||||
setRelationships(parsedDiagram.relationships);
|
||||
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) {
|
||||
console.log(e);
|
||||
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 = async (gistId) => {
|
||||
const getRevisions = useCallback(
|
||||
async (gistId) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { data } = await getCommits(gistId);
|
||||
const { data } = await getCommitsWithFile(gistId, VERSION_FILENAME);
|
||||
|
||||
setRevisions(
|
||||
data.filter((version) => version.change_status.total !== 0),
|
||||
);
|
||||
@@ -56,55 +123,72 @@ export default function Revisions({ open }) {
|
||||
} finally {
|
||||
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) {
|
||||
getRevisions(gistId);
|
||||
}
|
||||
}, [gistId, t, open]);
|
||||
|
||||
if (gistId && isLoading) {
|
||||
return (
|
||||
<div className="text-blue-500 text-center">
|
||||
<Spin size="middle" />
|
||||
<div>{t("loading")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [gistId, getRevisions, open]);
|
||||
|
||||
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={() => {}}>
|
||||
<Button icon={<IconPlus />} block onClick={recordVersion}>
|
||||
{t("record_version")}
|
||||
</Button>
|
||||
<IconButton icon={<IconChevronRight />} title="Next" />
|
||||
</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>
|
||||
{isLoading ? (
|
||||
<div className="text-blue-500 text-center mt-3">
|
||||
<Spin size="middle" />
|
||||
<div>{t("loading")}</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>
|
||||
);
|
||||
|
@@ -5,7 +5,7 @@ import Todo from "./Todo";
|
||||
import Revisions from "./Revisions";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Sidesheet({ type, onClose }) {
|
||||
export default function Sidesheet({ type, title, setTitle, onClose }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function getTitle(type) {
|
||||
@@ -28,7 +28,13 @@ export default function Sidesheet({ type, onClose }) {
|
||||
case SIDESHEET.TODO:
|
||||
return <Todo />;
|
||||
case SIDESHEET.REVISIONS:
|
||||
return <Revisions open={type !== SIDESHEET.NONE} />;
|
||||
return (
|
||||
<Revisions
|
||||
open={type !== SIDESHEET.NONE}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { databases } from "../data/databases";
|
||||
import { isRtl } from "../i18n/utils/rtl";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { get } from "../api/gists";
|
||||
import { get, SHARE_FILENAME } from "../api/gists";
|
||||
|
||||
export const IdContext = createContext({
|
||||
gistId: "",
|
||||
@@ -293,7 +293,7 @@ export default function WorkSpace() {
|
||||
const loadFromGist = async (shareId) => {
|
||||
try {
|
||||
const { data } = await get(shareId);
|
||||
const parsedDiagram = JSON.parse(data.files["share.json"].content);
|
||||
const parsedDiagram = JSON.parse(data.files[SHARE_FILENAME].content);
|
||||
setUndoStack([]);
|
||||
setRedoStack([]);
|
||||
setGistId(shareId);
|
||||
@@ -372,6 +372,12 @@ export default function WorkSpace() {
|
||||
searchParams,
|
||||
]);
|
||||
|
||||
const returnToCurrentDiagram = async () => {
|
||||
await load();
|
||||
setLayout((prev) => ({ ...prev, readOnly: false }));
|
||||
setVersion(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
tables?.length === 0 &&
|
||||
@@ -447,15 +453,20 @@ export default function WorkSpace() {
|
||||
<Canvas saveState={saveState} setSaveState={setSaveState} />
|
||||
</CanvasContextProvider>
|
||||
{version && (
|
||||
<div
|
||||
className="absolute right-8 top-2"
|
||||
onClick={() => setShowRestoreModal(true)}
|
||||
>
|
||||
<div className="absolute right-8 top-2 space-x-2">
|
||||
<Button
|
||||
icon={<i className="fa-solid fa-rotate-right mt-0.5"></i>}
|
||||
onClick={() => setShowRestoreModal(true)}
|
||||
>
|
||||
{t("restore_version")}
|
||||
</Button>
|
||||
<Button
|
||||
type="tertiary"
|
||||
onClick={returnToCurrentDiagram}
|
||||
icon={<i className="bi bi-arrow-return-right mt-1"></i>}
|
||||
>
|
||||
{t("return_to_current")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!(layout.sidebar || layout.toolbar || layout.header) && (
|
||||
@@ -521,7 +532,8 @@ export default function WorkSpace() {
|
||||
onCancel={() => setShowRestoreModal(false)}
|
||||
title={
|
||||
<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>
|
||||
}
|
||||
okText={t("continue")}
|
||||
|
@@ -267,7 +267,8 @@ 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 the current changes.",
|
||||
return_to_current: "Return to diagram"
|
||||
},
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user