From 1a4a6b55d680d7c8e4c064d20faaaf1cb524b32a Mon Sep 17 00:00:00 2001
From: 1ilit <1ilit@proton.me>
Date: Sat, 5 Apr 2025 02:02:51 +0400
Subject: [PATCH 1/9] clean up sidesheet

---
 src/assets/calendar.png                       | Bin 12228 -> 0 bytes
 src/assets/process.png                        | Bin 23468 -> 0 bytes
 src/assets/process_dark.png                   | Bin 18824 -> 0 bytes
 src/components/EditorHeader/ControlPanel.jsx  |   8 +++++
 .../EditorHeader/SideSheet/Revisions.jsx      |   5 +++
 .../EditorHeader/SideSheet/Sidesheet.jsx      |  34 ++++++------------
 .../EditorHeader/SideSheet/Timeline.jsx       |   4 +--
 .../EditorHeader/SideSheet/Todo.jsx           |   6 ++--
 src/data/constants.js                         |   1 +
 src/i18n/locales/en.js                        |   1 +
 10 files changed, 30 insertions(+), 29 deletions(-)
 delete mode 100644 src/assets/calendar.png
 delete mode 100644 src/assets/process.png
 delete mode 100644 src/assets/process_dark.png
 create mode 100644 src/components/EditorHeader/SideSheet/Revisions.jsx

diff --git a/src/assets/calendar.png b/src/assets/calendar.png
deleted file mode 100644
index 132b77817ce6f91726f7777c8ec091d0ae50b4db..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/src/assets/process.png b/src/assets/process.png
deleted file mode 100644
index 37474f3d24885c0a0e426030aea637688365ea6f..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/src/assets/process_dark.png b/src/assets/process_dark.png
deleted file mode 100644
index 6370cb17c8b588b22aa3682cd9d5a0f87d4fafb2..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx
index cf9893456..ddc68d0e7 100644
--- a/src/components/EditorHeader/ControlPanel.jsx
+++ b/src/components/EditorHeader/ControlPanel.jsx
@@ -1652,6 +1652,14 @@ export default function ControlPanel({
               <IconSaveStroked size="extra-large" />
             </button>
           </Tooltip>
+          <Tooltip content={t("revisions")} position="bottom">
+            <button
+              className="py-1 px-2 hover-2 rounded-sm text-xl -mt-0.5"
+              onClick={() => setSidesheet(SIDESHEET.REVISIONS)}
+            >
+              <i className="fa-solid fa-code-branch" />{" "}
+            </button>
+          </Tooltip>
           <Tooltip content={t("to_do")} position="bottom">
             <button
               className="py-1 px-2 hover-2 rounded-sm text-xl -mt-0.5"
diff --git a/src/components/EditorHeader/SideSheet/Revisions.jsx b/src/components/EditorHeader/SideSheet/Revisions.jsx
new file mode 100644
index 000000000..dfcb67d8b
--- /dev/null
+++ b/src/components/EditorHeader/SideSheet/Revisions.jsx
@@ -0,0 +1,5 @@
+export default function Revisions() {
+  return (
+    <div className="sidesheet-theme">Revisions</div>
+  )
+}
diff --git a/src/components/EditorHeader/SideSheet/Sidesheet.jsx b/src/components/EditorHeader/SideSheet/Sidesheet.jsx
index c01c6429b..9e1c51f4e 100644
--- a/src/components/EditorHeader/SideSheet/Sidesheet.jsx
+++ b/src/components/EditorHeader/SideSheet/Sidesheet.jsx
@@ -1,37 +1,21 @@
 import { SideSheet as SemiUISideSheet } from "@douyinfe/semi-ui";
 import { SIDESHEET } from "../../../data/constants";
-import { useSettings } from "../../../hooks";
-import timeLine from "../../../assets/process.png";
-import timeLineDark from "../../../assets/process_dark.png";
-import todo from "../../../assets/calendar.png";
 import Timeline from "./Timeline";
 import Todo from "./Todo";
+import Revisions from "./Revisions";
 import { useTranslation } from "react-i18next";
 
 export default function Sidesheet({ type, onClose }) {
   const { t } = useTranslation();
-  const { settings } = useSettings();
 
   function getTitle(type) {
     switch (type) {
       case SIDESHEET.TIMELINE:
-        return (
-          <div className="flex items-center">
-            <img
-              src={settings.mode === "light" ? timeLine : timeLineDark}
-              className="w-7"
-              alt="chat icon"
-            />
-            <div className="ms-3 text-lg">{t("timeline")}</div>
-          </div>
-        );
+        return t("timeline");
       case SIDESHEET.TODO:
-        return (
-          <div className="flex items-center">
-            <img src={todo} className="w-7" alt="todo icon" />
-            <div className="ms-3 text-lg">{t("to_do")}</div>
-          </div>
-        );
+        return t("to_do");
+      case SIDESHEET.REVISIONS:
+        return t("revisions");
       default:
         break;
     }
@@ -43,6 +27,8 @@ export default function Sidesheet({ type, onClose }) {
         return <Timeline />;
       case SIDESHEET.TODO:
         return <Todo />;
+      case SIDESHEET.REVISIONS:
+        return <Revisions />;
       default:
         break;
     }
@@ -52,12 +38,12 @@ export default function Sidesheet({ type, onClose }) {
     <SemiUISideSheet
       visible={type !== SIDESHEET.NONE}
       onCancel={onClose}
-      width={340}
-      title={getTitle(type)}
+      width={420}
+      title={<div className="text-lg">{getTitle(type)}</div>}
       style={{ paddingBottom: "16px" }}
       bodyStyle={{ padding: "0px" }}
     >
-      {getContent(type)}
+      <div className="sidesheet-theme">{getContent(type)}</div>
     </SemiUISideSheet>
   );
 }
diff --git a/src/components/EditorHeader/SideSheet/Timeline.jsx b/src/components/EditorHeader/SideSheet/Timeline.jsx
index bd962ec41..22fe83099 100644
--- a/src/components/EditorHeader/SideSheet/Timeline.jsx
+++ b/src/components/EditorHeader/SideSheet/Timeline.jsx
@@ -8,7 +8,7 @@ export default function Timeline() {
 
   if (undoStack.length > 0) {
     return (
-      <List className="sidesheet-theme">
+      <List>
         {[...undoStack].reverse().map((e, i) => (
           <List.Item
             key={i}
@@ -24,6 +24,6 @@ export default function Timeline() {
       </List>
     );
   } else {
-    return <div className="m-5 sidesheet-theme">{t("no_activity")}</div>;
+    return <div className="m-5">{t("no_activity")}</div>;
   }
 }
diff --git a/src/components/EditorHeader/SideSheet/Todo.jsx b/src/components/EditorHeader/SideSheet/Todo.jsx
index 6f000ddea..03f20a571 100644
--- a/src/components/EditorHeader/SideSheet/Todo.jsx
+++ b/src/components/EditorHeader/SideSheet/Todo.jsx
@@ -106,7 +106,7 @@ export default function Todo() {
 
   return (
     <>
-      <div className="flex justify-between items-center mx-5 mb-2 sidesheet-theme">
+      <div className="flex justify-between items-center mx-5 mb-2">
         <Dropdown
           render={
             <Dropdown.Menu>
@@ -153,7 +153,7 @@ export default function Todo() {
         </Button>
       </div>
       {tasks.length > 0 ? (
-        <List className="sidesheet-theme">
+        <List>
           {tasks.map((task, i) => (
             <List.Item
               key={i}
@@ -267,7 +267,7 @@ export default function Todo() {
           ))}
         </List>
       ) : (
-        <div className="m-5 sidesheet-theme">{t("no_tasks")}</div>
+        <div className="m-5">{t("no_tasks")}</div>
       )}
     </>
   );
diff --git a/src/data/constants.js b/src/data/constants.js
index 6cd0202d5..33c1504bf 100644
--- a/src/data/constants.js
+++ b/src/data/constants.js
@@ -98,6 +98,7 @@ export const SIDESHEET = {
   NONE: 0,
   TODO: 1,
   TIMELINE: 2,
+  REVISIONS: 3,
 };
 
 export const DB = {
diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js
index 9d401a377..c6439e4c0 100644
--- a/src/i18n/locales/en.js
+++ b/src/i18n/locales/en.js
@@ -256,6 +256,7 @@ const en = {
     export_saved_data: "Export saved data",
     dbml_view: "DBML view",
     tab_view: "Tab view",
+    revisions: "Revisions",
   },
 };
 

From bce70bb01b0f89c7900a98006fdf0b5fa80f6263 Mon Sep 17 00:00:00 2001
From: 1ilit <1ilit@proton.me>
Date: Sat, 5 Apr 2025 15:41:26 +0400
Subject: [PATCH 2/9] clean up sharing

---
 src/components/EditorHeader/Modal/Share.jsx   |  1 +
 .../EditorHeader/SideSheet/Revisions.jsx      | 32 +++++++++++++++++--
 src/components/Workspace.jsx                  |  4 +--
 src/i18n/locales/en.js                        |  1 +
 4 files changed, 34 insertions(+), 4 deletions(-)

diff --git a/src/components/EditorHeader/Modal/Share.jsx b/src/components/EditorHeader/Modal/Share.jsx
index 87f843c00..b72e2966f 100644
--- a/src/components/EditorHeader/Modal/Share.jsx
+++ b/src/components/EditorHeader/Modal/Share.jsx
@@ -66,6 +66,7 @@ export default function Share({ title, setModal }) {
 
   useEffect(() => {
     const updateOrGenerateLink = async () => {
+      setLoading(true);
       try {
         setLoading(true);
         if (!gistId || gistId === "") {
diff --git a/src/components/EditorHeader/SideSheet/Revisions.jsx b/src/components/EditorHeader/SideSheet/Revisions.jsx
index dfcb67d8b..9d23e2135 100644
--- a/src/components/EditorHeader/SideSheet/Revisions.jsx
+++ b/src/components/EditorHeader/SideSheet/Revisions.jsx
@@ -1,5 +1,33 @@
+import { useContext, useEffect, useState } from "react";
+import { IdContext } from "../../Workspace";
+import { useTranslation } from "react-i18next";
+import { Button, Spin } from "@douyinfe/semi-ui";
+import { IconPlus } from "@douyinfe/semi-icons";
+
 export default function Revisions() {
+  const { gistId } = useContext(IdContext);
+  const { t } = useTranslation();
+  const [isLoading, setIsLoading] = useState(true);
+
+  useEffect(() => {
+    setIsLoading(false);
+  }, []);
+
+  if (gistId && isLoading) {
+    return (
+      <div className="text-blue-500 text-center">
+        <Spin size="middle" />
+        <div>{t("loading")}</div>
+      </div>
+    );
+  }
+
   return (
-    <div className="sidesheet-theme">Revisions</div>
-  )
+    <div className="mx-5">
+      <Button icon={<IconPlus />} block onClick={() => {}}>
+        {t("record_version")}
+      </Button>
+      {!gistId && <div className="my-3">{t("no_saved_revisions")}</div>}
+    </div>
+  );
 }
diff --git a/src/components/Workspace.jsx b/src/components/Workspace.jsx
index fd24554c7..1f3a220b2 100644
--- a/src/components/Workspace.jsx
+++ b/src/components/Workspace.jsx
@@ -287,8 +287,8 @@ export default function WorkSpace() {
 
     const loadFromGist = async (shareId) => {
       try {
-        const res = await get(shareId);
-        const diagramSrc = res.data.files["share.json"].content;
+        const gist = await get(shareId);
+        const diagramSrc = gist.files["share.json"].content;
         const d = JSON.parse(diagramSrc);
         setGistId(shareId);
         setUndoStack([]);
diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js
index c6439e4c0..bab23e330 100644
--- a/src/i18n/locales/en.js
+++ b/src/i18n/locales/en.js
@@ -257,6 +257,7 @@ const en = {
     dbml_view: "DBML view",
     tab_view: "Tab view",
     revisions: "Revisions",
+    no_saved_revisions: "No saved revisions",
   },
 };
 

From f2a9362c75ccf15d92086a71a2d2930c018743cd Mon Sep 17 00:00:00 2001
From: 1ilit <1ilit@proton.me>
Date: Sun, 6 Apr 2025 02:35:46 +0400
Subject: [PATCH 3/9] add revisions sidesheet

---
 package-lock.json                             | 10 +++
 package.json                                  |  1 +
 src/api/gists.js                              | 14 ++++
 .../EditorHeader/SideSheet/Revisions.jsx      | 76 ++++++++++++++++---
 .../EditorHeader/SideSheet/Sidesheet.jsx      |  2 +-
 src/i18n/locales/en.js                        |  2 +
 src/index.css                                 |  7 +-
 7 files changed, 100 insertions(+), 12 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 2af627597..51661e3fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,6 +30,7 @@
         "jspdf": "^3.0.1",
         "jszip": "^3.10.1",
         "lexical": "^0.12.5",
+        "moment": "^2.30.1",
         "nanoid": "^5.1.5",
         "node-sql-parser": "^5.3.10",
         "oracle-sql-parser": "^0.1.0",
@@ -6223,6 +6224,15 @@
         "node": "*"
       }
     },
+    "node_modules/moment": {
+      "version": "2.30.1",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/monaco-editor": {
       "version": "0.52.2",
       "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
diff --git a/package.json b/package.json
index 07c7cece5..7a4e091f5 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
     "jszip": "^3.10.1",
     "lexical": "^0.12.5",
     "nanoid": "^5.1.5",
+    "moment": "^2.30.1",
     "node-sql-parser": "^5.3.10",
     "oracle-sql-parser": "^0.1.0",
     "react": "^18.2.0",
diff --git a/src/api/gists.js b/src/api/gists.js
index 1a3cd4058..3f931b3f0 100644
--- a/src/api/gists.js
+++ b/src/api/gists.js
@@ -32,3 +32,17 @@ export async function get(gistId) {
 
   return res.data;
 }
+
+export async function getCommits(gistId, perPage = 20, page = 1) {
+  const res = await octokit.request(
+    `GET /gists/${gistId}/commits?per_page=${perPage}&page=${page}`,
+    {
+      gist_id: gistId,
+      headers: {
+        "X-GitHub-Api-Version": "2022-11-28",
+      },
+    },
+  );
+
+  return res.data;
+}
diff --git a/src/components/EditorHeader/SideSheet/Revisions.jsx b/src/components/EditorHeader/SideSheet/Revisions.jsx
index 9d23e2135..779bb55dd 100644
--- a/src/components/EditorHeader/SideSheet/Revisions.jsx
+++ b/src/components/EditorHeader/SideSheet/Revisions.jsx
@@ -1,17 +1,40 @@
 import { useContext, useEffect, useState } from "react";
 import { IdContext } from "../../Workspace";
 import { useTranslation } from "react-i18next";
-import { Button, Spin } from "@douyinfe/semi-ui";
-import { IconPlus } from "@douyinfe/semi-icons";
+import { Button, IconButton, Spin, Steps, Tag, Toast } from "@douyinfe/semi-ui";
+import {
+  IconPlus,
+  IconChevronRight,
+  IconChevronLeft,
+} from "@douyinfe/semi-icons";
+import * as gists from "../../../api/gists";
+import moment from "moment";
 
-export default function Revisions() {
+export default function Revisions({ open }) {
   const { gistId } = useContext(IdContext);
   const { t } = useTranslation();
-  const [isLoading, setIsLoading] = useState(true);
+  const [isLoading, setIsLoading] = useState(false);
+  const [revisions, setRevisions] = useState([]);
 
   useEffect(() => {
-    setIsLoading(false);
-  }, []);
+    const getRevisions = async (gistId) => {
+      setIsLoading(true);
+      try {
+        const gist = await gists.getCommits(gistId);
+        setRevisions(gist);
+        console.log(gist);
+      } catch (e) {
+        console.log(e);
+        Toast.error(t("oops_smth_went_wrong"));
+      } finally {
+        setIsLoading(false);
+      }
+    };
+
+    if (gistId && open) {
+      getRevisions(gistId);
+    }
+  }, [gistId, t, open]);
 
   if (gistId && isLoading) {
     return (
@@ -23,11 +46,44 @@ export default function Revisions() {
   }
 
   return (
-    <div className="mx-5">
-      <Button icon={<IconPlus />} block onClick={() => {}}>
-        {t("record_version")}
-      </Button>
+    <div className="mx-5 relative h-full">
+      <div className="sticky top-0 z-10 sidesheet-theme pb-2 flex gap-2">
+        <IconButton icon={<IconChevronLeft />} />
+        <Button icon={<IconPlus />} block onClick={() => {}}>
+          {t("record_version")}
+        </Button>
+        <IconButton icon={<IconChevronRight />} />
+      </div>
       {!gistId && <div className="my-3">{t("no_saved_revisions")}</div>}
+      {gistId && (
+        <div className="my-3 overflow-y-auto">
+          <Steps
+            direction="vertical"
+            type="basic"
+            onChange={(i) => console.log(i)}
+          >
+            {revisions.map((r, i) => (
+              <Steps.Step
+                key={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={
+                  <div>
+                    <div>{`Commited on ${moment(
+                      new Date(r.committed_at),
+                    ).format("MMMM Do YYYY, h:mm")}`}</div>
+                  </div>
+                }
+                icon={<i className="text-sm fa-solid fa-asterisk" />}
+              />
+            ))}
+          </Steps>
+        </div>
+      )}
     </div>
   );
 }
diff --git a/src/components/EditorHeader/SideSheet/Sidesheet.jsx b/src/components/EditorHeader/SideSheet/Sidesheet.jsx
index 9e1c51f4e..f3784aad8 100644
--- a/src/components/EditorHeader/SideSheet/Sidesheet.jsx
+++ b/src/components/EditorHeader/SideSheet/Sidesheet.jsx
@@ -28,7 +28,7 @@ export default function Sidesheet({ type, onClose }) {
       case SIDESHEET.TODO:
         return <Todo />;
       case SIDESHEET.REVISIONS:
-        return <Revisions />;
+        return <Revisions open={type !== SIDESHEET.NONE} />;
       default:
         break;
     }
diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js
index bab23e330..e823c4dab 100644
--- a/src/i18n/locales/en.js
+++ b/src/i18n/locales/en.js
@@ -258,6 +258,8 @@ const en = {
     tab_view: "Tab view",
     revisions: "Revisions",
     no_saved_revisions: "No saved revisions",
+    version: "Version",
+    record_version: "Record version",
   },
 };
 
diff --git a/src/index.css b/src/index.css
index e0b3da508..0edbc6375 100644
--- a/src/index.css
+++ b/src/index.css
@@ -67,7 +67,12 @@
   background-color: rgba(var(--semi-blue-6), 1);
 }
 
-.semi-spin-wrapper {
+.semi-steps-item-content,
+.semi-steps-item-title {
+  width: 100%;
+}
+
+.semi-spin-wrapper  {
   color: inherit;
 }
 

From 592549580862bfc868458548ec6c8909ea58a50c Mon Sep 17 00:00:00 2001
From: 1ilit <1ilit@proton.me>
Date: Sun, 13 Jul 2025 16:42:15 +0400
Subject: [PATCH 4/9] update getCommits and clean up

---
 src/api/gists.js                              | 15 +++++-----
 .../EditorHeader/SideSheet/Revisions.jsx      |  7 ++---
 src/components/Workspace.jsx                  | 29 +++++++++----------
 3 files changed, 24 insertions(+), 27 deletions(-)

diff --git a/src/api/gists.js b/src/api/gists.js
index 3f931b3f0..64ec45981 100644
--- a/src/api/gists.js
+++ b/src/api/gists.js
@@ -34,15 +34,14 @@ export async function get(gistId) {
 }
 
 export async function getCommits(gistId, perPage = 20, page = 1) {
-  const res = await octokit.request(
-    `GET /gists/${gistId}/commits?per_page=${perPage}&page=${page}`,
-    {
-      gist_id: gistId,
-      headers: {
-        "X-GitHub-Api-Version": "2022-11-28",
-      },
+  const res = await axios.get(`${baseUrl}/gists/${gistId}/commits`, {
+    params: {
+      per_page: perPage,
+      page,
     },
-  );
+  });
+
+  console.log(res)
 
   return res.data;
 }
diff --git a/src/components/EditorHeader/SideSheet/Revisions.jsx b/src/components/EditorHeader/SideSheet/Revisions.jsx
index 779bb55dd..936788934 100644
--- a/src/components/EditorHeader/SideSheet/Revisions.jsx
+++ b/src/components/EditorHeader/SideSheet/Revisions.jsx
@@ -7,7 +7,7 @@ import {
   IconChevronRight,
   IconChevronLeft,
 } from "@douyinfe/semi-icons";
-import * as gists from "../../../api/gists";
+import { getCommits } from "../../../api/gists";
 import moment from "moment";
 
 export default function Revisions({ open }) {
@@ -20,9 +20,8 @@ export default function Revisions({ open }) {
     const getRevisions = async (gistId) => {
       setIsLoading(true);
       try {
-        const gist = await gists.getCommits(gistId);
-        setRevisions(gist);
-        console.log(gist);
+        const { data } = await getCommits(gistId);
+        setRevisions(data);
       } catch (e) {
         console.log(e);
         Toast.error(t("oops_smth_went_wrong"));
diff --git a/src/components/Workspace.jsx b/src/components/Workspace.jsx
index 1f3a220b2..a9792027c 100644
--- a/src/components/Workspace.jsx
+++ b/src/components/Workspace.jsx
@@ -287,25 +287,24 @@ export default function WorkSpace() {
 
     const loadFromGist = async (shareId) => {
       try {
-        const gist = await get(shareId);
-        const diagramSrc = gist.files["share.json"].content;
-        const d = JSON.parse(diagramSrc);
-        setGistId(shareId);
+        const { data } = await get(shareId);
+        const parsedDiagram = JSON.parse(data.files["share.json"].content);
         setUndoStack([]);
         setRedoStack([]);
+        setGistId(shareId);
         setLoadedFromGistId(shareId);
-        setDatabase(d.database);
-        setTitle(d.title);
-        setTables(d.tables);
-        setRelationships(d.relationships);
-        setNotes(d.notes);
-        setAreas(d.subjectAreas);
-        setTransform(d.transform);
-        if (databases[d.database].hasTypes) {
-          setTypes(d.types ?? []);
+        setDatabase(parsedDiagram.database);
+        setTitle(parsedDiagram.title);
+        setTables(parsedDiagram.tables);
+        setRelationships(parsedDiagram.relationships);
+        setNotes(parsedDiagram.notes);
+        setAreas(parsedDiagram.subjectAreas);
+        setTransform(parsedDiagram.transform);
+        if (databases[parsedDiagram.database].hasTypes) {
+          setTypes(parsedDiagram.types ?? []);
         }
-        if (databases[d.database].hasEnums) {
-          setEnums(d.enums ?? []);
+        if (databases[parsedDiagram.database].hasEnums) {
+          setEnums(parsedDiagram.enums ?? []);
         }
       } catch (e) {
         console.log(e);

From 0d0bd3e63f3f8fce912b31152170e98209c2913a Mon Sep 17 00:00:00 2001
From: 1ilit <1ilit@proton.me>
Date: Wed, 16 Jul 2025 20:37:53 +0400
Subject: [PATCH 5/9] update date localization

---
 package-lock.json                             | 20 +++++------
 package.json                                  |  2 +-
 src/api/gists.js                              |  2 --
 src/components/EditorHeader/Modal/Share.jsx   |  1 -
 .../EditorHeader/SideSheet/Revisions.jsx      | 35 +++++++++----------
 src/i18n/i18n.js                              | 14 ++++----
 src/i18n/locales/en.js                        |  4 ++-
 src/index.css                                 |  2 +-
 8 files changed, 39 insertions(+), 41 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 51661e3fd..a571c291a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,7 @@
         "jspdf": "^3.0.1",
         "jszip": "^3.10.1",
         "lexical": "^0.12.5",
-        "moment": "^2.30.1",
+        "luxon": "^3.7.1",
         "nanoid": "^5.1.5",
         "node-sql-parser": "^5.3.10",
         "oracle-sql-parser": "^0.1.0",
@@ -5162,6 +5162,15 @@
         "yallist": "^3.0.2"
       }
     },
+    "node_modules/luxon": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",
+      "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/markdown-extensions": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
@@ -6224,15 +6233,6 @@
         "node": "*"
       }
     },
-    "node_modules/moment": {
-      "version": "2.30.1",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
-      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
-      "license": "MIT",
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/monaco-editor": {
       "version": "0.52.2",
       "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
diff --git a/package.json b/package.json
index 7a4e091f5..1b17ae4ac 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,8 @@
     "jspdf": "^3.0.1",
     "jszip": "^3.10.1",
     "lexical": "^0.12.5",
+    "luxon": "^3.7.1",
     "nanoid": "^5.1.5",
-    "moment": "^2.30.1",
     "node-sql-parser": "^5.3.10",
     "oracle-sql-parser": "^0.1.0",
     "react": "^18.2.0",
diff --git a/src/api/gists.js b/src/api/gists.js
index 64ec45981..d62c79e83 100644
--- a/src/api/gists.js
+++ b/src/api/gists.js
@@ -41,7 +41,5 @@ export async function getCommits(gistId, perPage = 20, page = 1) {
     },
   });
 
-  console.log(res)
-
   return res.data;
 }
diff --git a/src/components/EditorHeader/Modal/Share.jsx b/src/components/EditorHeader/Modal/Share.jsx
index b72e2966f..87f843c00 100644
--- a/src/components/EditorHeader/Modal/Share.jsx
+++ b/src/components/EditorHeader/Modal/Share.jsx
@@ -66,7 +66,6 @@ export default function Share({ title, setModal }) {
 
   useEffect(() => {
     const updateOrGenerateLink = async () => {
-      setLoading(true);
       try {
         setLoading(true);
         if (!gistId || gistId === "") {
diff --git a/src/components/EditorHeader/SideSheet/Revisions.jsx b/src/components/EditorHeader/SideSheet/Revisions.jsx
index 936788934..ca1fc5a2b 100644
--- a/src/components/EditorHeader/SideSheet/Revisions.jsx
+++ b/src/components/EditorHeader/SideSheet/Revisions.jsx
@@ -8,20 +8,22 @@ import {
   IconChevronLeft,
 } from "@douyinfe/semi-icons";
 import { getCommits } from "../../../api/gists";
-import moment from "moment";
+import { DateTime } from "luxon";
 
 export default function Revisions({ open }) {
   const { gistId } = useContext(IdContext);
-  const { t } = useTranslation();
+  const { t, i18n } = useTranslation();
   const [isLoading, setIsLoading] = useState(false);
   const [revisions, setRevisions] = useState([]);
 
   useEffect(() => {
     const getRevisions = async (gistId) => {
-      setIsLoading(true);
       try {
+        setIsLoading(true);
         const { data } = await getCommits(gistId);
-        setRevisions(data);
+        setRevisions(
+          data.filter((version) => version.change_status.total !== 0),
+        );
       } catch (e) {
         console.log(e);
         Toast.error(t("oops_smth_went_wrong"));
@@ -47,36 +49,33 @@ export default function Revisions({ 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 />} />
+        <IconButton icon={<IconChevronLeft />} title="Previous" />
         <Button icon={<IconPlus />} block onClick={() => {}}>
           {t("record_version")}
         </Button>
-        <IconButton icon={<IconChevronRight />} />
+        <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"
-            onChange={(i) => console.log(i)}
-          >
+          <Steps direction="vertical" type="basic">
             {revisions.map((r, i) => (
               <Steps.Step
                 key={r.version}
+                onClick={() => {
+                  alert(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={
-                  <div>
-                    <div>{`Commited on ${moment(
-                      new Date(r.committed_at),
-                    ).format("MMMM Do YYYY, h:mm")}`}</div>
-                  </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" />}
               />
             ))}
diff --git a/src/i18n/i18n.js b/src/i18n/i18n.js
index c78cff2bc..aabbdb016 100644
--- a/src/i18n/i18n.js
+++ b/src/i18n/i18n.js
@@ -35,10 +35,10 @@ import { pl, polish } from "./locales/pl";
 import { no, norwegian } from "./locales/no";
 import { sv, swedish } from "./locales/sv-se";
 import { ur, urdu } from "./locales/ur";
-import { jp, japanese} from "./locales/jp"
-import {ne, nepali} from "./locales/ne"
-import {ug, uyghur} from "./locales/ug";
-import {pa_pk,punjabipk  } from "./locales/pa-pk";
+import { jp, japanese } from "./locales/jp";
+import { ne, nepali } from "./locales/ne";
+import { ug, uyghur } from "./locales/ug";
+import { pa_pk, punjabipk } from "./locales/pa-pk";
 import { cz, czech } from "./locales/cz";
 
 export const languages = [
@@ -80,7 +80,7 @@ export const languages = [
   nepali,
   uyghur,
   punjabipk,
-  czech
+  czech,
 ].sort((a, b) => a.name.localeCompare(b.name));
 
 i18n
@@ -130,8 +130,8 @@ i18n
       jp,
       ne,
       ug,
-      "pa-PK":pa_pk,
-      cz
+      "pa-PK": pa_pk,
+      cz,
     },
   });
 
diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js
index e823c4dab..b387b8603 100644
--- a/src/i18n/locales/en.js
+++ b/src/i18n/locales/en.js
@@ -9,7 +9,8 @@ const en = {
     report_bug: "Report a bug",
     import: "Import",
     inherits: "Inherits",
-    merging_column_w_inherited_definition: "Column '{{fieldName}}' in table '{{tableName}}' with inherited definition will be merged",
+    merging_column_w_inherited_definition:
+      "Column '{{fieldName}}' in table '{{tableName}}' with inherited definition will be merged",
     import_from: "Import from",
     file: "File",
     new: "New",
@@ -260,6 +261,7 @@ const en = {
     no_saved_revisions: "No saved revisions",
     version: "Version",
     record_version: "Record version",
+    commited_at: "Commited at",
   },
 };
 
diff --git a/src/index.css b/src/index.css
index 0edbc6375..697313f67 100644
--- a/src/index.css
+++ b/src/index.css
@@ -69,7 +69,7 @@
 
 .semi-steps-item-content,
 .semi-steps-item-title {
-  width: 100%;
+  width: 100% !important;
 }
 
 .semi-spin-wrapper  {

From 03e17914d50d2252ed5ff1bac270fab62f3b33bd Mon Sep 17 00:00:00 2001
From: 1ilit <1ilit@proton.me>
Date: Sat, 19 Jul 2025 21:11:58 +0400
Subject: [PATCH 6/9] load diagram in read only mode from previous version

---
 src/api/gists.js                              |  6 +++
 src/components/EditorCanvas/Area.jsx          |  3 ++
 src/components/EditorCanvas/Canvas.jsx        | 11 +++--
 src/components/EditorHeader/ControlPanel.jsx  | 48 ++++++++++++-------
 src/components/EditorHeader/Modal/Rename.jsx  |  3 ++
 .../EditorHeader/Modal/SetTableWidth.jsx      |  4 +-
 src/components/EditorHeader/Modal/Share.jsx   |  2 +-
 .../EditorHeader/SideSheet/Revisions.jsx      | 36 +++++++++++---
 .../EditorSidePanel/AreasTab/AreaDetails.jsx  |  8 +++-
 .../EditorSidePanel/ColorPicker.jsx           |  4 +-
 .../EditorSidePanel/EnumsTab/EnumDetails.jsx  | 13 +++--
 .../EditorSidePanel/EnumsTab/EnumsTab.jsx     | 10 +++-
 .../EditorSidePanel/NotesTab/NoteInfo.jsx     |  9 +++-
 .../EditorSidePanel/NotesTab/NotesTab.jsx     | 10 +++-
 .../RelationshipsTab/RelationshipInfo.jsx     | 14 ++++--
 .../TablesTab/FieldDetails.jsx                | 19 ++++++--
 .../TablesTab/IndexDetails.jsx                | 12 +++--
 .../EditorSidePanel/TablesTab/TableField.jsx  | 22 +++++++--
 .../EditorSidePanel/TablesTab/TableInfo.jsx   | 20 ++++++--
 .../EditorSidePanel/TablesTab/TablesTab.jsx   | 14 ++++--
 .../EditorSidePanel/TypesTab/TypeField.jsx    | 23 +++++++--
 .../EditorSidePanel/TypesTab/TypeInfo.jsx     | 11 +++--
 .../EditorSidePanel/TypesTab/TypesTab.jsx     | 10 +++-
 src/components/SortableList/DragHandle.jsx    |  7 +--
 src/components/Workspace.jsx                  | 40 +++++++++++++---
 src/context/LayoutContext.jsx                 |  1 +
 26 files changed, 281 insertions(+), 79 deletions(-)

diff --git a/src/api/gists.js b/src/api/gists.js
index d62c79e83..87c15e9d1 100644
--- a/src/api/gists.js
+++ b/src/api/gists.js
@@ -43,3 +43,9 @@ export async function getCommits(gistId, perPage = 20, page = 1) {
 
   return res.data;
 }
+
+export async function getVersion(gistId, sha) {
+  const res = await axios.get(`${baseUrl}/gists/${gistId}/${sha}`);
+
+  return res.data;
+}
diff --git a/src/components/EditorCanvas/Area.jsx b/src/components/EditorCanvas/Area.jsx
index 533d37c6d..2bf07fa46 100644
--- a/src/components/EditorCanvas/Area.jsx
+++ b/src/components/EditorCanvas/Area.jsx
@@ -221,6 +221,7 @@ function EditPopoverContent({ data }) {
   const { updateArea, deleteArea } = useAreas();
   const { setUndoStack, setRedoStack } = useUndoRedo();
   const { t } = useTranslation();
+  const {layout} = useLayout();
   const initialColorRef = useRef(data.color);
 
   const handleColorPick = (color) => {
@@ -266,6 +267,7 @@ function EditPopoverContent({ data }) {
           value={data.name}
           placeholder={t("name")}
           className="me-2"
+          readOnly={layout.readOnly}
           onChange={(value) => updateArea(data.id, { name: value })}
           onFocus={(e) => setEditField({ name: e.target.value })}
           onBlur={(e) => {
@@ -289,6 +291,7 @@ function EditPopoverContent({ data }) {
         />
         <ColorPicker
           usePopover={true}
+          readOnly={true}
           value={data.color}
           onChange={(color) => updateArea(data.id, { color })}
           onColorPick={(color) => handleColorPick(color)}
diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx
index 6ddffe856..1680aa294 100644
--- a/src/components/EditorCanvas/Canvas.jsx
+++ b/src/components/EditorCanvas/Canvas.jsx
@@ -254,21 +254,24 @@ export default function Canvas() {
 
     if (!e.isPrimary) return;
 
+    
     if (panning.isPanning) {
       setTransform((prev) => ({
         ...prev,
         pan: {
           x:
-            panning.panStart.x +
-            (panning.cursorStart.x - pointer.spaces.screen.x) / transform.zoom,
+          panning.panStart.x +
+          (panning.cursorStart.x - pointer.spaces.screen.x) / transform.zoom,
           y:
-            panning.panStart.y +
-            (panning.cursorStart.y - pointer.spaces.screen.y) / transform.zoom,
+          panning.panStart.y +
+          (panning.cursorStart.y - pointer.spaces.screen.y) / transform.zoom,
         },
       }));
       return;
     }
 
+    if(layout.readOnly) return;
+
     if (linking) {
       setLinkingLine({
         ...linkingLine,
diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx
index ddc68d0e7..78219761d 100644
--- a/src/components/EditorHeader/ControlPanel.jsx
+++ b/src/components/EditorHeader/ControlPanel.jsx
@@ -125,7 +125,7 @@ export default function ControlPanel({
   const { selectedElement, setSelectedElement } = useSelect();
   const { transform, setTransform } = useTransform();
   const { t, i18n } = useTranslation();
-  const { setGistId } = useContext(IdContext);
+  const { version, setGistId } = useContext(IdContext);
   const navigate = useNavigate();
 
   const invertLayout = (component) =>
@@ -1746,7 +1746,7 @@ export default function ControlPanel({
                 />
               )}
               <div
-                className="text-xl  me-1"
+                className="text-xl flex items-center gap-1 me-1"
                 onPointerEnter={(e) => e.isPrimary && setShowEditName(true)}
                 onPointerLeave={(e) => e.isPrimary && setShowEditName(false)}
                 onPointerDown={(e) => {
@@ -1756,12 +1756,20 @@ export default function ControlPanel({
                 }}
                 onClick={() => setModal(MODAL.RENAME)}
               >
-                {window.name.split(" ")[0] === "t" ? "Templates/" : "Diagrams/"}
-                {title}
+                <span>
+                  {(window.name.split(" ")[0] === "t"
+                    ? "Templates/"
+                    : "Diagrams/") + title}
+                </span>
+                {version && (
+                  <Tag className="mt-1" color="blue" size="small">
+                    {version.substring(0, 7)}
+                  </Tag>
+                )}
               </div>
               {(showEditName || modal === MODAL.RENAME) && <IconEdit />}
             </div>
-            <div className="flex justify-between items-center">
+            <div className="flex items-center">
               <div className="flex justify-start text-md select-none me-2">
                 {Object.keys(menu).map((category) => (
                   <Dropdown
@@ -1874,17 +1882,25 @@ export default function ControlPanel({
                   </Dropdown>
                 ))}
               </div>
-              <Button
-                size="small"
-                type="tertiary"
-                icon={
-                  saveState === State.LOADING || saveState === State.SAVING ? (
-                    <Spin size="small" />
-                  ) : null
-                }
-              >
-                {getState()}
-              </Button>
+              {layout.readOnly && (
+                <Tag size="small">
+                  {t("read_only")}
+                </Tag>
+              )}
+              {!layout.readOnly && (
+                <Tag
+                  size="small"
+                  type="light"
+                  prefixIcon={
+                    saveState === State.LOADING ||
+                    saveState === State.SAVING ? (
+                      <Spin size="small" />
+                    ) : null
+                  }
+                >
+                  {getState()}
+                </Tag>
+              )}
             </div>
           </div>
         </div>
diff --git a/src/components/EditorHeader/Modal/Rename.jsx b/src/components/EditorHeader/Modal/Rename.jsx
index 522d5e63d..3a8861e96 100644
--- a/src/components/EditorHeader/Modal/Rename.jsx
+++ b/src/components/EditorHeader/Modal/Rename.jsx
@@ -1,14 +1,17 @@
 import { Input } from "@douyinfe/semi-ui";
 import { useTranslation } from "react-i18next";
+import { useLayout } from "../../../hooks";
 
 export default function Rename({ title, setTitle }) {
   const { t } = useTranslation();
+  const { layout } = useLayout();
 
   return (
     <Input
       placeholder={t("name")}
       defaultValue={title}
       onChange={(v) => setTitle(v)}
+      readonly={layout.readOnly}
     />
   );
 }
diff --git a/src/components/EditorHeader/Modal/SetTableWidth.jsx b/src/components/EditorHeader/Modal/SetTableWidth.jsx
index 93482e68e..aed463c2f 100644
--- a/src/components/EditorHeader/Modal/SetTableWidth.jsx
+++ b/src/components/EditorHeader/Modal/SetTableWidth.jsx
@@ -1,13 +1,15 @@
 import { InputNumber } from "@douyinfe/semi-ui";
-import { useSettings } from "../../../hooks";
+import { useLayout, useSettings } from "../../../hooks";
 
 export default function SetTableWidth() {
+  const { layout } = useLayout();
   const { settings, setSettings } = useSettings();
 
   return (
     <InputNumber
       className="w-full"
       value={settings.tableWidth}
+      readonly={layout.readOnly}
       onChange={(c) => {
         if (c < 180) return;
         setSettings((prev) => ({ ...prev, tableWidth: c }));
diff --git a/src/components/EditorHeader/Modal/Share.jsx b/src/components/EditorHeader/Modal/Share.jsx
index 87f843c00..582669722 100644
--- a/src/components/EditorHeader/Modal/Share.jsx
+++ b/src/components/EditorHeader/Modal/Share.jsx
@@ -116,7 +116,7 @@ export default function Share({ title, setModal }) {
       {!error && (
         <>
           <div className="flex gap-3">
-            <Input value={url} size="large" />
+            <Input value={url} size="large" readonly />
           </div>
           <div className="text-xs mt-2">{t("share_info")}</div>
           <div className="flex gap-2 mt-3">
diff --git a/src/components/EditorHeader/SideSheet/Revisions.jsx b/src/components/EditorHeader/SideSheet/Revisions.jsx
index ca1fc5a2b..4a312efc3 100644
--- a/src/components/EditorHeader/SideSheet/Revisions.jsx
+++ b/src/components/EditorHeader/SideSheet/Revisions.jsx
@@ -1,4 +1,4 @@
-import { useContext, useEffect, useState } from "react";
+import { useCallback, useContext, useEffect, useState } from "react";
 import { IdContext } from "../../Workspace";
 import { useTranslation } from "react-i18next";
 import { Button, IconButton, Spin, Steps, Tag, Toast } from "@douyinfe/semi-ui";
@@ -7,15 +7,41 @@ import {
   IconChevronRight,
   IconChevronLeft,
 } from "@douyinfe/semi-icons";
-import { getCommits } from "../../../api/gists";
+import { getCommits, getVersion } from "../../../api/gists";
 import { DateTime } from "luxon";
+import { useAreas, useDiagram, useLayout } from "../../../hooks";
 
 export default function Revisions({ open }) {
-  const { gistId } = useContext(IdContext);
+  const { gistId, setVersion } = useContext(IdContext);
+  const { setAreas } = useAreas();
+  const { setLayout } = useLayout();
+  const { setTables, setRelationships } = useDiagram();
   const { t, i18n } = useTranslation();
   const [isLoading, setIsLoading] = useState(false);
   const [revisions, setRevisions] = useState([]);
 
+  const loadVersion = useCallback(
+    async (sha) => {
+      try {
+        const version = await getVersion(gistId, sha);
+        setVersion(sha);
+        setLayout((prev) => ({ ...prev, readOnly: true }));
+
+        const content = version.data.files["share.json"].content;
+
+        const parsedDiagram = JSON.parse(content);
+
+        setTables(parsedDiagram.tables);
+        setRelationships(parsedDiagram.relationships);
+        setAreas(parsedDiagram.subjectAreas);
+      } catch (e) {
+        console.log(e);
+        Toast.error("failed_to_load_diagram");
+      }
+    },
+    [gistId, setTables, setRelationships, setAreas, setVersion, setLayout],
+  );
+
   useEffect(() => {
     const getRevisions = async (gistId) => {
       try {
@@ -62,9 +88,7 @@ export default function Revisions({ open }) {
             {revisions.map((r, i) => (
               <Steps.Step
                 key={r.version}
-                onClick={() => {
-                  alert(r.version);
-                }}
+                onClick={() => loadVersion(r.version)}
                 title={
                   <div className="flex justify-between items-center w-full">
                     <span>{`${t("version")} ${revisions.length - i}`}</span>
diff --git a/src/components/EditorSidePanel/AreasTab/AreaDetails.jsx b/src/components/EditorSidePanel/AreasTab/AreaDetails.jsx
index 0a62a31cb..7b1ceac3f 100644
--- a/src/components/EditorSidePanel/AreasTab/AreaDetails.jsx
+++ b/src/components/EditorSidePanel/AreasTab/AreaDetails.jsx
@@ -2,12 +2,13 @@ import { useState, useRef } from "react";
 import { Button, Input } from "@douyinfe/semi-ui";
 import ColorPicker from "../ColorPicker";
 import { IconDeleteStroked } from "@douyinfe/semi-icons";
-import { useAreas, useUndoRedo } from "../../../hooks";
+import { useAreas, useLayout, useUndoRedo } from "../../../hooks";
 import { Action, ObjectType } from "../../../data/constants";
 import { useTranslation } from "react-i18next";
 
 export default function AreaInfo({ data, i }) {
   const { t } = useTranslation();
+  const { layout } = useLayout();
   const { deleteArea, updateArea } = useAreas();
   const { setUndoStack, setRedoStack } = useUndoRedo();
   const [editField, setEditField] = useState({});
@@ -53,6 +54,7 @@ export default function AreaInfo({ data, i }) {
       <Input
         value={data.name}
         placeholder={t("name")}
+        readonly={layout.readOnly}
         onChange={(value) => updateArea(data.id, { name: value })}
         onFocus={(e) => setEditField({ name: e.target.value })}
         onBlur={(e) => {
@@ -77,12 +79,14 @@ export default function AreaInfo({ data, i }) {
       <ColorPicker
         usePopover={true}
         value={data.color}
+        readOnly={layout.readOnly}
         onChange={(color) => updateArea(i, { color })}
         onColorPick={(color) => handleColorPick(color)}
       />
       <Button
-        icon={<IconDeleteStroked />}
         type="danger"
+        disabled={layout.readOnly}
+        icon={<IconDeleteStroked />}
         onClick={() => deleteArea(i, true)}
       />
     </div>
diff --git a/src/components/EditorSidePanel/ColorPicker.jsx b/src/components/EditorSidePanel/ColorPicker.jsx
index c0ed31225..3bb4959c3 100644
--- a/src/components/EditorSidePanel/ColorPicker.jsx
+++ b/src/components/EditorSidePanel/ColorPicker.jsx
@@ -2,8 +2,9 @@ import { ColorPicker as SemiColorPicker } from "@douyinfe/semi-ui";
 import { useState } from "react";
 
 export default function ColorPicker({
-  children,
   value,
+  readOnly,
+  children,
   onChange,
   onColorPick,
   ...props
@@ -25,6 +26,7 @@ export default function ColorPicker({
         {...props}
         value={SemiColorPicker.colorStringToValue(value)}
         onChange={({ hex: color }) => {
+          if (readOnly) return;
           setPickedColor(color);
           onChange(color);
         }}
diff --git a/src/components/EditorSidePanel/EnumsTab/EnumDetails.jsx b/src/components/EditorSidePanel/EnumsTab/EnumDetails.jsx
index c84e18e04..4500275d3 100644
--- a/src/components/EditorSidePanel/EnumsTab/EnumDetails.jsx
+++ b/src/components/EditorSidePanel/EnumsTab/EnumDetails.jsx
@@ -1,12 +1,13 @@
 import { useState } from "react";
 import { Button, Input, TagInput } from "@douyinfe/semi-ui";
 import { IconDeleteStroked } from "@douyinfe/semi-icons";
-import { useDiagram, useEnums, useUndoRedo } from "../../../hooks";
+import { useDiagram, useEnums, useLayout, useUndoRedo } from "../../../hooks";
 import { Action, ObjectType } from "../../../data/constants";
 import { useTranslation } from "react-i18next";
 
 export default function EnumDetails({ data, i }) {
   const { t } = useTranslation();
+  const { layout } = useLayout();
   const { deleteEnum, updateEnum } = useEnums();
   const { tables, updateField } = useDiagram();
   const { setUndoStack, setRedoStack } = useUndoRedo();
@@ -18,6 +19,7 @@ export default function EnumDetails({ data, i }) {
         <div className="font-semibold">{t("Name")}: </div>
         <Input
           value={data.name}
+          readonly={layout.readOnly}
           placeholder={t("name")}
           validateStatus={data.name.trim() === "" ? "error" : "default"}
           onChange={(value) => {
@@ -71,7 +73,11 @@ export default function EnumDetails({ data, i }) {
         className="my-2"
         placeholder={t("values")}
         validateStatus={data.values.length === 0 ? "error" : "default"}
-        onChange={(v) => updateEnum(i, { values: v })}
+        onChange={(v) => {
+          if (layout.readOnly) return;
+
+          updateEnum(i, { values: v });
+        }}
         onFocus={() => setEditField({ values: data.values })}
         onBlur={() => {
           if (JSON.stringify(editField.values) === JSON.stringify(data.values))
@@ -95,8 +101,9 @@ export default function EnumDetails({ data, i }) {
       />
       <Button
         block
-        icon={<IconDeleteStroked />}
         type="danger"
+        icon={<IconDeleteStroked />}
+        disabled={layout.readOnly}
         onClick={() => deleteEnum(i, true)}
       >
         {t("delete")}
diff --git a/src/components/EditorSidePanel/EnumsTab/EnumsTab.jsx b/src/components/EditorSidePanel/EnumsTab/EnumsTab.jsx
index be3c32cfe..2ba8feae6 100644
--- a/src/components/EditorSidePanel/EnumsTab/EnumsTab.jsx
+++ b/src/components/EditorSidePanel/EnumsTab/EnumsTab.jsx
@@ -1,5 +1,5 @@
 import { Button, Collapse } from "@douyinfe/semi-ui";
-import { useEnums } from "../../../hooks";
+import { useEnums, useLayout } from "../../../hooks";
 import { IconPlus } from "@douyinfe/semi-icons";
 import { useTranslation } from "react-i18next";
 import SearchBar from "./SearchBar";
@@ -8,6 +8,7 @@ import Empty from "../Empty";
 
 export default function EnumsTab() {
   const { enums, addEnum } = useEnums();
+  const { layout } = useLayout();
   const { t } = useTranslation();
 
   return (
@@ -15,7 +16,12 @@ export default function EnumsTab() {
       <div className="flex gap-2">
         <SearchBar />
         <div>
-          <Button icon={<IconPlus />} block onClick={() => addEnum()}>
+          <Button
+            block
+            icon={<IconPlus />}
+            onClick={() => addEnum()}
+            disabled={layout.readOnly}
+          >
             {t("add_enum")}
           </Button>
         </div>
diff --git a/src/components/EditorSidePanel/NotesTab/NoteInfo.jsx b/src/components/EditorSidePanel/NotesTab/NoteInfo.jsx
index 98cd19355..adb4d7ec1 100644
--- a/src/components/EditorSidePanel/NotesTab/NoteInfo.jsx
+++ b/src/components/EditorSidePanel/NotesTab/NoteInfo.jsx
@@ -3,10 +3,11 @@ import { Button, Collapse, TextArea, Input } from "@douyinfe/semi-ui";
 import ColorPicker from "../ColorPicker";
 import { IconDeleteStroked } from "@douyinfe/semi-icons";
 import { Action, ObjectType } from "../../../data/constants";
-import { useNotes, useUndoRedo } from "../../../hooks";
+import { useLayout, useNotes, useUndoRedo } from "../../../hooks";
 import { useTranslation } from "react-i18next";
 
 export default function NoteInfo({ data, nid }) {
+  const { layout } = useLayout();
   const { updateNote, deleteNote } = useNotes();
   const { setUndoStack, setRedoStack } = useUndoRedo();
   const [editField, setEditField] = useState({});
@@ -62,6 +63,7 @@ export default function NoteInfo({ data, nid }) {
         <div className="font-semibold me-2 break-keep">{t("title")}:</div>
         <Input
           value={data.title}
+          readonly={layout.readOnly}
           placeholder={t("title")}
           onChange={(value) => updateNote(data.id, { title: value })}
           onFocus={(e) => setEditField({ title: e.target.value })}
@@ -90,6 +92,7 @@ export default function NoteInfo({ data, nid }) {
           placeholder={t("content")}
           value={data.content}
           autosize
+          readonly={layout.readOnly}
           onChange={(value) => {
             const textarea = document.getElementById(`note_${data.id}`);
             textarea.style.height = "0";
@@ -127,13 +130,15 @@ export default function NoteInfo({ data, nid }) {
         <div className="ms-2 flex flex-col gap-2">
           <ColorPicker
             usePopover={true}
+            readOnly={layout.readOnly}
             value={data.color}
             onChange={(color) => updateNote(data.id, { color })}
             onColorPick={(color) => handleColorPick(color)}
           />
           <Button
-            icon={<IconDeleteStroked />}
             type="danger"
+            disabled={layout.readOnly}
+            icon={<IconDeleteStroked />}
             onClick={() => deleteNote(nid, true)}
           />
         </div>
diff --git a/src/components/EditorSidePanel/NotesTab/NotesTab.jsx b/src/components/EditorSidePanel/NotesTab/NotesTab.jsx
index 07b68600a..5b6b0338a 100644
--- a/src/components/EditorSidePanel/NotesTab/NotesTab.jsx
+++ b/src/components/EditorSidePanel/NotesTab/NotesTab.jsx
@@ -1,6 +1,6 @@
 import { Button, Collapse } from "@douyinfe/semi-ui";
 import { IconPlus } from "@douyinfe/semi-icons";
-import { useNotes, useSelect } from "../../../hooks";
+import { useLayout, useNotes, useSelect } from "../../../hooks";
 import Empty from "../Empty";
 import SearchBar from "./SearchBar";
 import NoteInfo from "./NoteInfo";
@@ -10,6 +10,7 @@ export default function NotesTab() {
   const { notes, addNote } = useNotes();
   const { selectedElement, setSelectedElement } = useSelect();
   const { t } = useTranslation();
+  const { layout } = useLayout();
 
   return (
     <>
@@ -23,7 +24,12 @@ export default function NotesTab() {
           }
         />
         <div>
-          <Button icon={<IconPlus />} block onClick={() => addNote()}>
+          <Button
+            block
+            icon={<IconPlus />}
+            onClick={() => addNote()}
+            disabled={layout.readOnly}
+          >
             {t("add_note")}
           </Button>
         </div>
diff --git a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx
index be15be11e..a158ccc22 100644
--- a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx
+++ b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx
@@ -18,7 +18,7 @@ import {
   Action,
   ObjectType,
 } from "../../../data/constants";
-import { useDiagram, useUndoRedo } from "../../../hooks";
+import { useDiagram, useLayout, useUndoRedo } from "../../../hooks";
 import i18n from "../../../i18n/i18n";
 import { useTranslation } from "react-i18next";
 import { useMemo, useState } from "react";
@@ -38,6 +38,7 @@ export default function RelationshipInfo({ data }) {
   const { setUndoStack, setRedoStack } = useUndoRedo();
   const { tables, deleteRelationship, updateRelationship } = useDiagram();
   const { t } = useTranslation();
+  const { layout } = useLayout();
   const [editField, setEditField] = useState({});
 
   const relValues = useMemo(() => {
@@ -98,6 +99,8 @@ export default function RelationshipInfo({ data }) {
   };
 
   const changeCardinality = (value) => {
+    if (layout.readOnly) return;
+
     setUndoStack((prev) => [
       ...prev,
       {
@@ -117,6 +120,8 @@ export default function RelationshipInfo({ data }) {
   };
 
   const changeConstraint = (key, value) => {
+    if (layout.readOnly) return;
+
     const undoKey = `${key}Constraint`;
     setUndoStack((prev) => [
       ...prev,
@@ -145,6 +150,7 @@ export default function RelationshipInfo({ data }) {
           validateStatus={data.name.trim() === "" ? "error" : "default"}
           placeholder={t("name")}
           className="ms-2"
+          readonly={layout.readOnly}
           onChange={(value) => updateRelationship(data.id, { name: value })}
           onFocus={(e) => setEditField({ name: e.target.value })}
           onBlur={(e) => {
@@ -196,9 +202,10 @@ export default function RelationshipInfo({ data }) {
                 />
                 <div className="mt-2">
                   <Button
-                    icon={<IconLoopTextStroked />}
                     block
+                    icon={<IconLoopTextStroked />}
                     onClick={swapKeys}
+                    disabled={layout.readOnly}
                   >
                     {t("swap")}
                   </Button>
@@ -250,9 +257,10 @@ export default function RelationshipInfo({ data }) {
         </Col>
       </Row>
       <Button
-        icon={<IconDeleteStroked />}
         block
         type="danger"
+        disabled={layout.readOnly}
+        icon={<IconDeleteStroked />}
         onClick={() => deleteRelationship(data.id)}
       >
         {t("delete")}
diff --git a/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx b/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx
index 0dc80cc0e..93b2c333e 100644
--- a/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx
+++ b/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx
@@ -9,13 +9,14 @@ import {
 } from "@douyinfe/semi-ui";
 import { Action, ObjectType } from "../../../data/constants";
 import { IconDeleteStroked } from "@douyinfe/semi-icons";
-import { useDiagram, useUndoRedo } from "../../../hooks";
+import { useDiagram, useLayout, useUndoRedo } from "../../../hooks";
 import { useTranslation } from "react-i18next";
 import { dbToTypes } from "../../../data/datatypes";
 import { databases } from "../../../data/databases";
 
 export default function FieldDetails({ data, tid }) {
   const { t } = useTranslation();
+  const { layout } = useLayout();
   const { tables, database } = useDiagram();
   const { setUndoStack, setRedoStack } = useUndoRedo();
   const { updateField, deleteField } = useDiagram();
@@ -29,6 +30,7 @@ export default function FieldDetails({ data, tid }) {
         className="my-2"
         placeholder={t("default_value")}
         value={data.default}
+        readonly={layout.readOnly}
         disabled={dbToTypes[database][data.type].noDefault || data.increment}
         onChange={(value) => updateField(tid, data.id, { default: value })}
         onFocus={(e) => setEditField({ default: e.target.value })}
@@ -67,7 +69,10 @@ export default function FieldDetails({ data, tid }) {
             addOnBlur
             className="my-2"
             placeholder={t("use_for_batch_input")}
-            onChange={(v) => updateField(tid, data.id, { values: v })}
+            onChange={(v) => {
+              if (layout.readOnly) return;
+              updateField(tid, data.id, { values: v });
+            }}
             onFocus={() => setEditField({ values: data.values })}
             onBlur={() => {
               if (
@@ -102,6 +107,7 @@ export default function FieldDetails({ data, tid }) {
             className="my-2 w-full"
             placeholder={t("size")}
             value={data.size}
+            readonly={layout.readOnly}
             onChange={(value) => updateField(tid, data.id, { size: value })}
             onFocus={(e) => setEditField({ size: e.target.value })}
             onBlur={(e) => {
@@ -138,6 +144,7 @@ export default function FieldDetails({ data, tid }) {
                 ? "default"
                 : "error"
             }
+            readonly={layout.readOnly}
             value={data.size}
             onChange={(value) => updateField(tid, data.id, { size: value })}
             onFocus={(e) => setEditField({ size: e.target.value })}
@@ -172,6 +179,7 @@ export default function FieldDetails({ data, tid }) {
             placeholder={t("check")}
             value={data.check}
             disabled={data.increment}
+            readonly={layout.readOnly}
             onChange={(value) => updateField(tid, data.id, { check: value })}
             onFocus={(e) => setEditField({ check: e.target.value })}
             onBlur={(e) => {
@@ -203,6 +211,7 @@ export default function FieldDetails({ data, tid }) {
         <Checkbox
           value="unique"
           checked={data.unique}
+          disabled={layout.readOnly}
           onChange={(checkedValues) => {
             setUndoStack((prev) => [
               ...prev,
@@ -233,7 +242,7 @@ export default function FieldDetails({ data, tid }) {
           value="increment"
           checked={data.increment}
           disabled={
-            !dbToTypes[database][data.type].canIncrement || data.isArray
+            !dbToTypes[database][data.type].canIncrement || data.isArray || layout.readOnly
           }
           onChange={(checkedValues) => {
             setUndoStack((prev) => [
@@ -270,6 +279,7 @@ export default function FieldDetails({ data, tid }) {
           <Checkbox
             value="isArray"
             checked={data.isArray}
+            disabled={layout.readOnly}
             onChange={(checkedValues) => {
               setUndoStack((prev) => [
                 ...prev,
@@ -307,6 +317,7 @@ export default function FieldDetails({ data, tid }) {
             <Checkbox
               value="unsigned"
               checked={data.unsigned}
+              disabled={layout.readOnly}
               onChange={(checkedValues) => {
                 setUndoStack((prev) => [
                   ...prev,
@@ -343,6 +354,7 @@ export default function FieldDetails({ data, tid }) {
         className="my-2"
         placeholder={t("comment")}
         value={data.comment}
+        readonly={layout.readOnly}
         autosize
         rows={2}
         onChange={(value) => updateField(tid, data.id, { comment: value })}
@@ -372,6 +384,7 @@ export default function FieldDetails({ data, tid }) {
         icon={<IconDeleteStroked />}
         type="danger"
         block
+        disabled={layout.readOnly}
         onClick={() => deleteField(data, tid)}
       >
         {t("delete")}
diff --git a/src/components/EditorSidePanel/TablesTab/IndexDetails.jsx b/src/components/EditorSidePanel/TablesTab/IndexDetails.jsx
index 3774d86e9..45e85fba3 100644
--- a/src/components/EditorSidePanel/TablesTab/IndexDetails.jsx
+++ b/src/components/EditorSidePanel/TablesTab/IndexDetails.jsx
@@ -1,12 +1,13 @@
 import { Action, ObjectType } from "../../../data/constants";
 import { Input, Button, Popover, Checkbox, Select } from "@douyinfe/semi-ui";
 import { IconMore, IconDeleteStroked } from "@douyinfe/semi-icons";
-import { useDiagram, useUndoRedo } from "../../../hooks";
+import { useDiagram, useLayout, useUndoRedo } from "../../../hooks";
 import { useTranslation } from "react-i18next";
 import { useMemo, useState } from "react";
 
 export default function IndexDetails({ data, fields, iid, tid }) {
   const { t } = useTranslation();
+  const { layout } = useLayout();
   const { tables, updateTable } = useDiagram();
   const { setUndoStack, setRedoStack } = useUndoRedo();
   const [editField, setEditField] = useState({});
@@ -22,6 +23,8 @@ export default function IndexDetails({ data, fields, iid, tid }) {
         className="w-full"
         value={data.fields}
         onChange={(value) => {
+          if (layout.readOnly) return;
+
           setUndoStack((prev) => [
             ...prev,
             {
@@ -62,6 +65,7 @@ export default function IndexDetails({ data, fields, iid, tid }) {
             <Input
               value={data.name}
               placeholder={t("name")}
+              readonly={layout.readOnly}
               validateStatus={data.name.trim() === "" ? "error" : "default"}
               onFocus={() =>
                 setEditField({
@@ -106,6 +110,7 @@ export default function IndexDetails({ data, fields, iid, tid }) {
               <Checkbox
                 value="unique"
                 checked={data.unique}
+                disabled={layout.readOnly}
                 onChange={(checkedValues) => {
                   setUndoStack((prev) => [
                     ...prev,
@@ -145,9 +150,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
               ></Checkbox>
             </div>
             <Button
-              icon={<IconDeleteStroked />}
-              type="danger"
               block
+              type="danger"
+              disabled={layout.readOnly}
+              icon={<IconDeleteStroked />}
               onClick={() => {
                 setUndoStack((prev) => [
                   ...prev,
diff --git a/src/components/EditorSidePanel/TablesTab/TableField.jsx b/src/components/EditorSidePanel/TablesTab/TableField.jsx
index 13369fe90..b9495c773 100644
--- a/src/components/EditorSidePanel/TablesTab/TableField.jsx
+++ b/src/components/EditorSidePanel/TablesTab/TableField.jsx
@@ -2,7 +2,13 @@ import { useMemo, useState } from "react";
 import { Action, ObjectType } from "../../../data/constants";
 import { Input, Button, Popover, Select } from "@douyinfe/semi-ui";
 import { IconMore, IconKeyStroked } from "@douyinfe/semi-icons";
-import { useEnums, useDiagram, useTypes, useUndoRedo } from "../../../hooks";
+import {
+  useEnums,
+  useDiagram,
+  useTypes,
+  useUndoRedo,
+  useLayout,
+} from "../../../hooks";
 import { useTranslation } from "react-i18next";
 import { dbToTypes } from "../../../data/datatypes";
 import { DragHandle } from "../../SortableList/DragHandle";
@@ -12,6 +18,7 @@ export default function TableField({ data, tid, index, inherited }) {
   const { updateField } = useDiagram();
   const { types } = useTypes();
   const { enums } = useEnums();
+  const { layout } = useLayout();
   const { tables, database } = useDiagram();
   const { t } = useTranslation();
   const { setUndoStack, setRedoStack } = useUndoRedo();
@@ -20,7 +27,7 @@ export default function TableField({ data, tid, index, inherited }) {
 
   return (
     <div className="hover-1 my-2 flex gap-2 items-center">
-      <DragHandle id={data.id} />
+      <DragHandle readOnly={layout.readOnly} id={data.id} />
 
       <div className="min-w-20 flex-1/3">
         <Input
@@ -29,6 +36,7 @@ export default function TableField({ data, tid, index, inherited }) {
           validateStatus={
             data.name.trim() === "" || inherited ? "error" : "default"
           }
+          readonly={layout.readOnly}
           placeholder={t("name")}
           onChange={(value) => updateField(tid, data.id, { name: value })}
           onFocus={(e) => setEditField({ name: e.target.value })}
@@ -77,6 +85,8 @@ export default function TableField({ data, tid, index, inherited }) {
           validateStatus={data.type === "" ? "error" : "default"}
           placeholder={t("type")}
           onChange={(value) => {
+            if (layout.readOnly) return;
+
             if (value === data.type) return;
             setUndoStack((prev) => [
               ...prev,
@@ -142,10 +152,12 @@ export default function TableField({ data, tid, index, inherited }) {
 
       <div>
         <Button
-          type={data.notNull ? "tertiary" : "primary"}
           title={t("nullable")}
+          type={data.notNull ? "tertiary" : "primary"}
           theme={data.notNull ? "light" : "solid"}
           onClick={() => {
+            if (layout.readOnly) return;
+
             setUndoStack((prev) => [
               ...prev,
               {
@@ -172,11 +184,13 @@ export default function TableField({ data, tid, index, inherited }) {
 
       <div>
         <Button
-          type={data.primary ? "primary" : "tertiary"}
           title={t("primary")}
           theme={data.primary ? "solid" : "light"}
+          type={data.primary ? "primary" : "tertiary"}
           icon={<IconKeyStroked />}
           onClick={() => {
+            if (layout.readOnly) return;
+
             setUndoStack((prev) => [
               ...prev,
               {
diff --git a/src/components/EditorSidePanel/TablesTab/TableInfo.jsx b/src/components/EditorSidePanel/TablesTab/TableInfo.jsx
index b9f3706b8..ae89df5a2 100644
--- a/src/components/EditorSidePanel/TablesTab/TableInfo.jsx
+++ b/src/components/EditorSidePanel/TablesTab/TableInfo.jsx
@@ -9,7 +9,12 @@ import {
 } from "@douyinfe/semi-ui";
 import ColorPicker from "../ColorPicker";
 import { IconDeleteStroked } from "@douyinfe/semi-icons";
-import { useDiagram, useSaveState, useUndoRedo } from "../../../hooks";
+import {
+  useDiagram,
+  useLayout,
+  useSaveState,
+  useUndoRedo,
+} from "../../../hooks";
 import { Action, ObjectType, State, DB } from "../../../data/constants";
 import TableField from "./TableField";
 import IndexDetails from "./IndexDetails";
@@ -21,6 +26,7 @@ export default function TableInfo({ data }) {
   const { tables, database } = useDiagram();
   const { t } = useTranslation();
   const [indexActiveKey, setIndexActiveKey] = useState("");
+  const { layout } = useLayout();
   const { deleteTable, updateTable, setTables } = useDiagram();
   const { setUndoStack, setRedoStack } = useUndoRedo();
   const { setSaveState } = useSaveState();
@@ -82,6 +88,7 @@ export default function TableInfo({ data }) {
           validateStatus={data.name.trim() === "" ? "error" : "default"}
           placeholder={t("name")}
           className="ms-2"
+          readonly={layout.readOnly}
           onChange={(value) => updateTable(data.id, { name: value })}
           onFocus={(e) => setEditField({ name: e.target.value })}
           onBlur={(e) => {
@@ -139,6 +146,8 @@ export default function TableInfo({ data }) {
               .filter((t) => t.id !== data.id)
               .map((t) => ({ label: t.name, value: t.name }))}
             onChange={(value) => {
+              if (layout.readOnly) return;
+
               setUndoStack((prev) => [
                 ...prev,
                 {
@@ -204,6 +213,7 @@ export default function TableInfo({ data }) {
             <TextArea
               field="comment"
               value={data.comment}
+              readonly={layout.readOnly}
               autosize
               placeholder={t("comment")}
               rows={1}
@@ -238,6 +248,7 @@ export default function TableInfo({ data }) {
       <div className="flex justify-between items-center gap-1 mb-2">
         <ColorPicker
           usePopover={true}
+          readOnly={layout.readOnly}
           value={data.color}
           onChange={(color) => updateTable(data.id, { color })}
           onColorPick={(color) => handleColorPick(color)}
@@ -245,6 +256,7 @@ export default function TableInfo({ data }) {
         <div className="flex gap-1">
           <Button
             block
+            disabled={layout.readOnly}
             onClick={() => {
               setIndexActiveKey("1");
               setUndoStack((prev) => [
@@ -277,6 +289,8 @@ export default function TableInfo({ data }) {
             {t("add_index")}
           </Button>
           <Button
+            block
+            disabled={layout.readOnly}
             onClick={() => {
               const id = nanoid();
               setUndoStack((prev) => [
@@ -312,13 +326,13 @@ export default function TableInfo({ data }) {
                 ],
               });
             }}
-            block
           >
             {t("add_field")}
           </Button>
           <Button
-            icon={<IconDeleteStroked />}
             type="danger"
+            disabled={layout.readOnly}
+            icon={<IconDeleteStroked />}
             onClick={() => deleteTable(data.id)}
           />
         </div>
diff --git a/src/components/EditorSidePanel/TablesTab/TablesTab.jsx b/src/components/EditorSidePanel/TablesTab/TablesTab.jsx
index 23914235c..42e7815bd 100644
--- a/src/components/EditorSidePanel/TablesTab/TablesTab.jsx
+++ b/src/components/EditorSidePanel/TablesTab/TablesTab.jsx
@@ -1,6 +1,6 @@
 import { Collapse, Button } from "@douyinfe/semi-ui";
 import { IconPlus } from "@douyinfe/semi-icons";
-import { useSelect, useDiagram, useSaveState } from "../../../hooks";
+import { useSelect, useDiagram, useSaveState, useLayout } from "../../../hooks";
 import { ObjectType, State } from "../../../data/constants";
 import { useTranslation } from "react-i18next";
 import { DragHandle } from "../../SortableList/DragHandle";
@@ -13,6 +13,7 @@ export default function TablesTab() {
   const { tables, addTable, setTables } = useDiagram();
   const { selectedElement, setSelectedElement } = useSelect();
   const { t } = useTranslation();
+  const { layout } = useLayout();
   const { setSaveState } = useSaveState();
 
   return (
@@ -20,7 +21,12 @@ export default function TablesTab() {
       <div className="flex gap-2">
         <SearchBar tables={tables} />
         <div>
-          <Button icon={<IconPlus />} block onClick={() => addTable()}>
+          <Button
+            block
+            icon={<IconPlus />}
+            onClick={() => addTable()}
+            disabled={layout.readOnly}
+          >
             {t("add_table")}
           </Button>
         </div>
@@ -60,6 +66,8 @@ export default function TablesTab() {
 }
 
 function TableListItem({ table }) {
+  const { layout } = useLayout();
+
   return (
     <div id={`scroll_table_${table.id}`}>
       <Collapse.Panel
@@ -67,7 +75,7 @@ function TableListItem({ table }) {
         header={
           <>
             <div className="flex items-center gap-2">
-              <DragHandle id={table.id} />
+              <DragHandle readOnly={layout.readOnly} id={table.id} />
               <div className="overflow-hidden text-ellipsis whitespace-nowrap">
                 {table.name}
               </div>
diff --git a/src/components/EditorSidePanel/TypesTab/TypeField.jsx b/src/components/EditorSidePanel/TypesTab/TypeField.jsx
index 2b186a0c3..f07be3558 100644
--- a/src/components/EditorSidePanel/TypesTab/TypeField.jsx
+++ b/src/components/EditorSidePanel/TypesTab/TypeField.jsx
@@ -11,13 +11,20 @@ import {
   Popover,
 } from "@douyinfe/semi-ui";
 import { IconDeleteStroked, IconMore } from "@douyinfe/semi-icons";
-import { useUndoRedo, useTypes, useDiagram, useEnums } from "../../../hooks";
+import {
+  useUndoRedo,
+  useTypes,
+  useDiagram,
+  useEnums,
+  useLayout,
+} from "../../../hooks";
 import { useTranslation } from "react-i18next";
 import { dbToTypes } from "../../../data/datatypes";
 
 export default function TypeField({ data, tid, fid }) {
   const { types, updateType } = useTypes();
   const { enums } = useEnums();
+  const { layout } = useLayout();
   const { database } = useDiagram();
   const { setUndoStack, setRedoStack } = useUndoRedo();
   const [editField, setEditField] = useState({});
@@ -28,6 +35,7 @@ export default function TypeField({ data, tid, fid }) {
       <Col span={10}>
         <Input
           value={data.name}
+          readonly={layout.readOnly}
           validateStatus={data.name === "" ? "error" : "default"}
           placeholder={t("name")}
           onChange={(value) =>
@@ -86,6 +94,7 @@ export default function TypeField({ data, tid, fid }) {
           validateStatus={data.type === "" ? "error" : "default"}
           placeholder={t("type")}
           onChange={(value) => {
+            if (layout.readOnly) return;
             if (value === data.type) return;
             setUndoStack((prev) => [
               ...prev,
@@ -160,13 +169,14 @@ export default function TypeField({ data, tid, fid }) {
                     }
                     className="my-2"
                     placeholder={t("use_for_batch_input")}
-                    onChange={(v) =>
+                    onChange={(v) => {
+                      if (layout.readOnly) return;
                       updateType(tid, {
                         fields: types[tid].fields.map((e, id) =>
                           id === fid ? { ...data, values: v } : e,
                         ),
-                      })
-                    }
+                      });
+                    }}
                     onFocus={() => setEditField({ values: data.values })}
                     onBlur={() => {
                       if (
@@ -202,6 +212,7 @@ export default function TypeField({ data, tid, fid }) {
                     className="my-2 w-full"
                     placeholder={t("size")}
                     value={data.size}
+                    readonly={layout.readOnly}
                     onChange={(value) =>
                       updateType(tid, {
                         fields: types[tid].fields.map((e, id) =>
@@ -239,6 +250,7 @@ export default function TypeField({ data, tid, fid }) {
                   <Input
                     className="my-2 w-full"
                     placeholder={t("set_precision")}
+                    readonly={layout.readOnly}
                     validateStatus={
                       /^\(\d+,\s*\d+\)$|^$/.test(data.size)
                         ? "default"
@@ -277,9 +289,10 @@ export default function TypeField({ data, tid, fid }) {
                 </>
               )}
               <Button
-                icon={<IconDeleteStroked />}
                 block
                 type="danger"
+                disabled={layout.readOnly}
+                icon={<IconDeleteStroked />}
                 onClick={() => {
                   setUndoStack((prev) => [
                     ...prev,
diff --git a/src/components/EditorSidePanel/TypesTab/TypeInfo.jsx b/src/components/EditorSidePanel/TypesTab/TypeInfo.jsx
index c05b75f17..f3c7337a5 100644
--- a/src/components/EditorSidePanel/TypesTab/TypeInfo.jsx
+++ b/src/components/EditorSidePanel/TypesTab/TypeInfo.jsx
@@ -10,11 +10,12 @@ import {
   Card,
 } from "@douyinfe/semi-ui";
 import { IconDeleteStroked, IconPlus } from "@douyinfe/semi-icons";
-import { useUndoRedo, useTypes, useDiagram } from "../../../hooks";
+import { useUndoRedo, useTypes, useDiagram, useLayout } from "../../../hooks";
 import TypeField from "./TypeField";
 import { useTranslation } from "react-i18next";
 
 export default function TypeInfo({ index, data }) {
+  const { layout } = useLayout();
   const { deleteType, updateType } = useTypes();
   const { tables, updateField } = useDiagram();
   const { setUndoStack, setRedoStack } = useUndoRedo();
@@ -35,6 +36,7 @@ export default function TypeInfo({ index, data }) {
           <div className="text-md font-semibold break-keep">{t("name")}: </div>
           <Input
             value={data.name}
+            readonly={layout.readOnly}
             validateStatus={data.name === "" ? "error" : "default"}
             placeholder={t("name")}
             className="ms-2"
@@ -97,6 +99,7 @@ export default function TypeInfo({ index, data }) {
                 field="comment"
                 value={data.comment}
                 autosize
+                readonly={layout.readOnly}
                 placeholder={t("comment")}
                 rows={1}
                 onChange={(value) =>
@@ -130,6 +133,7 @@ export default function TypeInfo({ index, data }) {
           <Col span={12}>
             <Button
               icon={<IconPlus />}
+              disabled={layout.readOnly}
               onClick={() => {
                 setUndoStack((prev) => [
                   ...prev,
@@ -162,10 +166,11 @@ export default function TypeInfo({ index, data }) {
           </Col>
           <Col span={12}>
             <Button
-              icon={<IconDeleteStroked />}
+              block
               type="danger"
+              disabled={layout.readOnly}
+              icon={<IconDeleteStroked />}
               onClick={() => deleteType(index)}
-              block
             >
               {t("delete")}
             </Button>
diff --git a/src/components/EditorSidePanel/TypesTab/TypesTab.jsx b/src/components/EditorSidePanel/TypesTab/TypesTab.jsx
index 80b85196f..4896ceb7b 100644
--- a/src/components/EditorSidePanel/TypesTab/TypesTab.jsx
+++ b/src/components/EditorSidePanel/TypesTab/TypesTab.jsx
@@ -1,6 +1,6 @@
 import { Collapse, Button, Popover } from "@douyinfe/semi-ui";
 import { IconPlus, IconInfoCircle } from "@douyinfe/semi-icons";
-import { useSelect, useDiagram, useTypes } from "../../../hooks";
+import { useSelect, useDiagram, useTypes, useLayout } from "../../../hooks";
 import { DB, ObjectType } from "../../../data/constants";
 import Searchbar from "./SearchBar";
 import Empty from "../Empty";
@@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next";
 export default function TypesTab() {
   const { types, addType } = useTypes();
   const { selectedElement, setSelectedElement } = useSelect();
+  const { layout } = useLayout();
   const { database } = useDiagram();
   const { t } = useTranslation();
 
@@ -18,7 +19,12 @@ export default function TypesTab() {
       <div className="flex gap-2">
         <Searchbar />
         <div>
-          <Button icon={<IconPlus />} block onClick={() => addType()}>
+          <Button
+            block
+            icon={<IconPlus />}
+            onClick={() => addType()}
+            disabled={layout.readOnly}
+          >
             {t("add_type")}
           </Button>
         </div>
diff --git a/src/components/SortableList/DragHandle.jsx b/src/components/SortableList/DragHandle.jsx
index 0fbdeca04..dc5b15a63 100644
--- a/src/components/SortableList/DragHandle.jsx
+++ b/src/components/SortableList/DragHandle.jsx
@@ -1,12 +1,13 @@
 import { IconHandle } from "@douyinfe/semi-icons";
 import { useSortable } from "@dnd-kit/sortable";
 
-export function DragHandle({ id }) {
+export function DragHandle({ id, readOnly }) {
   const { listeners } = useSortable({ id });
+
   return (
     <div
-      className="flex cursor-move items-center justify-center opacity-50 mt-0.5"
-      {...listeners}
+      className={`opacity-50 mt-0.5 ${readOnly ? "cursor-not-allowed" : "cursor-move"}`}
+      {...(!readOnly && listeners)}
     >
       <IconHandle />
     </div>
diff --git a/src/components/Workspace.jsx b/src/components/Workspace.jsx
index a9792027c..1cb7d0858 100644
--- a/src/components/Workspace.jsx
+++ b/src/components/Workspace.jsx
@@ -19,26 +19,33 @@ import {
   useEnums,
 } from "../hooks";
 import FloatingControls from "./FloatingControls";
-import { Modal, Tag } from "@douyinfe/semi-ui";
+import { Button, Modal, Tag } from "@douyinfe/semi-ui";
 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";
 
-export const IdContext = createContext({ gistId: "", setGistId: () => {} });
+export const IdContext = createContext({
+  gistId: "",
+  setGistId: () => {},
+  version: "",
+  setVersion: () => {},
+});
 
 const SIDEPANEL_MIN_WIDTH = 384;
 
 export default function WorkSpace() {
   const [id, setId] = useState(0);
   const [gistId, setGistId] = useState("");
+  const [version, setVersion] = useState("");
   const [loadedFromGistId, setLoadedFromGistId] = useState("");
   const [title, setTitle] = useState("Untitled Diagram");
   const [resize, setResize] = useState(false);
   const [width, setWidth] = useState(SIDEPANEL_MIN_WIDTH);
   const [lastSaved, setLastSaved] = useState("");
   const [showSelectDbModal, setShowSelectDbModal] = useState(false);
+  const [showRestoreModal, setShowRestoreModal] = useState(false);
   const [selectedDb, setSelectedDb] = useState("");
   const { layout } = useLayout();
   const { settings } = useSettings();
@@ -67,8 +74,6 @@ export default function WorkSpace() {
   };
 
   const save = useCallback(async () => {
-    if (saveState !== State.SAVING) return;
-
     const name = window.name.split(" ");
     const op = name[0];
     const saveAsDiagram = window.name === "" || op === "d" || op === "lt";
@@ -163,7 +168,6 @@ export default function WorkSpace() {
     enums,
     gistId,
     loadedFromGistId,
-    saveState,
   ]);
 
   const load = useCallback(async () => {
@@ -397,8 +401,12 @@ export default function WorkSpace() {
   ]);
 
   useEffect(() => {
+    if (layout.readOnly) return;
+
+    if (saveState !== State.SAVING) return;
+
     save();
-  }, [saveState, save]);
+  }, [saveState, layout, save]);
 
   useEffect(() => {
     document.title = "Editor | drawDB";
@@ -408,7 +416,7 @@ export default function WorkSpace() {
 
   return (
     <div className="h-full flex flex-col overflow-hidden theme">
-      <IdContext.Provider value={{ gistId, setGistId }}>
+      <IdContext.Provider value={{ gistId, setGistId, version, setVersion }}>
         <ControlPanel
           diagramId={id}
           setDiagramId={setId}
@@ -437,6 +445,18 @@ export default function WorkSpace() {
           <CanvasContextProvider className="h-full w-full">
             <Canvas saveState={saveState} setSaveState={setSaveState} />
           </CanvasContextProvider>
+          {version && (
+            <div
+              className="absolute right-8 top-2"
+              onClick={() => setShowRestoreModal(true)}
+            >
+              <Button
+                icon={<i className="fa-solid fa-rotate-right mt-0.5"></i>}
+              >
+                Restore version
+              </Button>
+            </div>
+          )}
           {!(layout.sidebar || layout.toolbar || layout.header) && (
             <div className="fixed right-5 bottom-4">
               <FloatingControls />
@@ -493,6 +513,12 @@ export default function WorkSpace() {
           ))}
         </div>
       </Modal>
+      <Modal
+        visible={showRestoreModal}
+        centered
+        closable
+        onCancel={() => setShowRestoreModal(false)}
+      ></Modal>
     </div>
   );
 }
diff --git a/src/context/LayoutContext.jsx b/src/context/LayoutContext.jsx
index 5349b0440..ec9ae61a5 100644
--- a/src/context/LayoutContext.jsx
+++ b/src/context/LayoutContext.jsx
@@ -9,6 +9,7 @@ export default function LayoutContextProvider({ children }) {
     issues: true,
     toolbar: true,
     dbmlEditor: false,
+    readOnly: false,
   });
 
   return (

From 382faab0ae0bda6d71a2bc82c0c743e9fa2b87be Mon Sep 17 00:00:00 2001
From: 1ilit <1ilit@proton.me>
Date: Sun, 20 Jul 2025 03:42:42 +0400
Subject: [PATCH 7/9] disable input from control panel and popovers

---
 src/components/EditorCanvas/Area.jsx          |  8 +++--
 src/components/EditorCanvas/Note.jsx          |  9 +++--
 src/components/EditorCanvas/Table.jsx         |  2 ++
 src/components/EditorHeader/ControlPanel.jsx  | 34 ++++++++-----------
 .../EditorSidePanel/AreasTab/AreasTab.jsx     | 10 ++++--
 5 files changed, 37 insertions(+), 26 deletions(-)

diff --git a/src/components/EditorCanvas/Area.jsx b/src/components/EditorCanvas/Area.jsx
index 2bf07fa46..14eedbdd0 100644
--- a/src/components/EditorCanvas/Area.jsx
+++ b/src/components/EditorCanvas/Area.jsx
@@ -143,6 +143,7 @@ export default function Area({
                     backgroundColor: "#2F68ADB3",
                   }}
                   onClick={lockUnlockArea}
+                  disabled={layout.readOnly}
                 />
                 <Popover
                   visible={areaIsOpen() && !layout.sidebar}
@@ -221,7 +222,7 @@ function EditPopoverContent({ data }) {
   const { updateArea, deleteArea } = useAreas();
   const { setUndoStack, setRedoStack } = useUndoRedo();
   const { t } = useTranslation();
-  const {layout} = useLayout();
+  const { layout } = useLayout();
   const initialColorRef = useRef(data.color);
 
   const handleColorPick = (color) => {
@@ -267,7 +268,7 @@ function EditPopoverContent({ data }) {
           value={data.name}
           placeholder={t("name")}
           className="me-2"
-          readOnly={layout.readOnly}
+          readonly={layout.readOnly}
           onChange={(value) => updateArea(data.id, { name: value })}
           onFocus={(e) => setEditField({ name: e.target.value })}
           onBlur={(e) => {
@@ -291,7 +292,7 @@ function EditPopoverContent({ data }) {
         />
         <ColorPicker
           usePopover={true}
-          readOnly={true}
+          readOnly={layout.readOnly}
           value={data.color}
           onChange={(color) => updateArea(data.id, { color })}
           onColorPick={(color) => handleColorPick(color)}
@@ -303,6 +304,7 @@ function EditPopoverContent({ data }) {
           type="danger"
           block
           onClick={() => deleteArea(data.id, true)}
+          disabled={layout.readOnly}
         >
           {t("delete")}
         </Button>
diff --git a/src/components/EditorCanvas/Note.jsx b/src/components/EditorCanvas/Note.jsx
index 31664c5af..7fd48d03c 100644
--- a/src/components/EditorCanvas/Note.jsx
+++ b/src/components/EditorCanvas/Note.jsx
@@ -213,6 +213,7 @@ export default function Note({ data, onPointerDown }) {
                     backgroundColor: "#2F68ADB3",
                   }}
                   onClick={lockUnlockNote}
+                  disabled={layout.readOnly}
                 />
                 <Popover
                   visible={
@@ -244,6 +245,7 @@ export default function Note({ data, onPointerDown }) {
                           value={data.title}
                           placeholder={t("title")}
                           className="me-2"
+                          readonly={layout.readOnly}
                           onChange={(value) =>
                             updateNote(data.id, { title: value })
                           }
@@ -271,6 +273,7 @@ export default function Note({ data, onPointerDown }) {
                         />
                         <ColorPicker
                           usePopover={true}
+                          readOnly={layout.readOnly}
                           value={data.color}
                           onChange={(color) => updateNote(data.id, { color })}
                           onColorPick={(color) => handleColorPick(color)}
@@ -278,9 +281,10 @@ export default function Note({ data, onPointerDown }) {
                       </div>
                       <div className="flex">
                         <Button
-                          icon={<IconDeleteStroked />}
-                          type="danger"
                           block
+                          type="danger"
+                          disabled={layout.readOnly}
+                          icon={<IconDeleteStroked />}
                           onClick={() => deleteNote(data.id, true)}
                         >
                           {t("delete")}
@@ -307,6 +311,7 @@ export default function Note({ data, onPointerDown }) {
           </div>
           <textarea
             id={`note_${data.id}`}
+            readOnly={layout.readOnly}
             value={data.content}
             onChange={handleChange}
             onFocus={(e) =>
diff --git a/src/components/EditorCanvas/Table.jsx b/src/components/EditorCanvas/Table.jsx
index 4c51fa149..c53deed25 100644
--- a/src/components/EditorCanvas/Table.jsx
+++ b/src/components/EditorCanvas/Table.jsx
@@ -134,6 +134,7 @@ export default function Table(props) {
                   style={{
                     backgroundColor: "#2f68adb3",
                   }}
+                  disabled={layout.readOnly}
                   onClick={lockUnlockTable}
                 />
                 <Button
@@ -197,6 +198,7 @@ export default function Table(props) {
                         block
                         style={{ marginTop: "8px" }}
                         onClick={() => deleteTable(tableData.id)}
+                        disabled={layout.readOnly}
                       >
                         {t("delete")}
                       </Button>
diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx
index 78219761d..f8485e90a 100644
--- a/src/components/EditorHeader/ControlPanel.jsx
+++ b/src/components/EditorHeader/ControlPanel.jsx
@@ -1598,47 +1598,46 @@ export default function ControlPanel({
           <Divider layout="vertical" margin="8px" />
           <Tooltip content={t("undo")} position="bottom">
             <button
-              className="py-1 px-2 hover-2 rounded-sm flex items-center"
+              className="py-1 px-2 hover-2 rounded-sm flex items-center disabled:opacity-50"
+              disabled={undoStack.length === 0}
               onClick={undo}
             >
-              <IconUndo
-                size="large"
-                style={{ color: undoStack.length === 0 ? "#9598a6" : "" }}
-              />
+              <IconUndo size="large" />
             </button>
           </Tooltip>
           <Tooltip content={t("redo")} position="bottom">
             <button
-              className="py-1 px-2 hover-2 rounded-sm flex items-center"
+              className="py-1 px-2 hover-2 rounded-sm flex items-center disabled:opacity-50"
+              disabled={redoStack.length === 0}
               onClick={redo}
             >
-              <IconRedo
-                size="large"
-                style={{ color: redoStack.length === 0 ? "#9598a6" : "" }}
-              />
+              <IconRedo size="large" />
             </button>
           </Tooltip>
           <Divider layout="vertical" margin="8px" />
           <Tooltip content={t("add_table")} position="bottom">
             <button
-              className="flex items-center py-1 px-2 hover-2 rounded-sm"
+              className="flex items-center py-1 px-2 hover-2 rounded-sm disabled:opacity-50"
               onClick={() => addTable()}
+              disabled={layout.readOnly}
             >
               <IconAddTable />
             </button>
           </Tooltip>
           <Tooltip content={t("add_area")} position="bottom">
             <button
-              className="py-1 px-2 hover-2 rounded-sm flex items-center"
+              className="py-1 px-2 hover-2 rounded-sm flex items-center disabled:opacity-50"
               onClick={() => addArea()}
+              disabled={layout.readOnly}
             >
               <IconAddArea />
             </button>
           </Tooltip>
           <Tooltip content={t("add_note")} position="bottom">
             <button
-              className="py-1 px-2 hover-2 rounded-sm flex items-center"
+              className="py-1 px-2 hover-2 rounded-sm flex items-center disabled:opacity-50"
               onClick={() => addNote()}
+              disabled={layout.readOnly}
             >
               <IconAddNote />
             </button>
@@ -1646,8 +1645,9 @@ export default function ControlPanel({
           <Divider layout="vertical" margin="8px" />
           <Tooltip content={t("save")} position="bottom">
             <button
-              className="py-1 px-2 hover-2 rounded-sm flex items-center"
+              className="py-1 px-2 hover-2 rounded-sm flex items-center disabled:opacity-50"
               onClick={save}
+              disabled={layout.readOnly}
             >
               <IconSaveStroked size="extra-large" />
             </button>
@@ -1882,11 +1882,7 @@ export default function ControlPanel({
                   </Dropdown>
                 ))}
               </div>
-              {layout.readOnly && (
-                <Tag size="small">
-                  {t("read_only")}
-                </Tag>
-              )}
+              {layout.readOnly && <Tag size="small">{t("read_only")}</Tag>}
               {!layout.readOnly && (
                 <Tag
                   size="small"
diff --git a/src/components/EditorSidePanel/AreasTab/AreasTab.jsx b/src/components/EditorSidePanel/AreasTab/AreasTab.jsx
index db28c7b36..756ea8531 100644
--- a/src/components/EditorSidePanel/AreasTab/AreasTab.jsx
+++ b/src/components/EditorSidePanel/AreasTab/AreasTab.jsx
@@ -1,13 +1,14 @@
 import { Button } from "@douyinfe/semi-ui";
 import { IconPlus } from "@douyinfe/semi-icons";
 import Empty from "../Empty";
-import { useAreas } from "../../../hooks";
+import { useAreas, useLayout } from "../../../hooks";
 import SearchBar from "./SearchBar";
 import AreaInfo from "./AreaDetails";
 import { useTranslation } from "react-i18next";
 
 export default function AreasTab() {
   const { areas, addArea } = useAreas();
+  const { layout } = useLayout();
   const { t } = useTranslation();
 
   return (
@@ -15,7 +16,12 @@ export default function AreasTab() {
       <div className="flex gap-2">
         <SearchBar />
         <div>
-          <Button icon={<IconPlus />} block onClick={() => addArea()}>
+          <Button
+            icon={<IconPlus />}
+            block
+            onClick={() => addArea()}
+            disabled={layout.readOnly}
+          >
             {t("add_area")}
           </Button>
         </div>

From 44d5ffb015d26657d8708b9d33c6fe4a82f5992b Mon Sep 17 00:00:00 2001
From: 1ilit <1ilit@proton.me>
Date: Thu, 24 Jul 2025 21:58:54 +0400
Subject: [PATCH 8/9] add restore warning modal

---
 src/components/Workspace.jsx | 21 ++++++++++++++++++---
 src/i18n/locales/en.js       |  4 ++++
 2 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/src/components/Workspace.jsx b/src/components/Workspace.jsx
index 1cb7d0858..873b83c80 100644
--- a/src/components/Workspace.jsx
+++ b/src/components/Workspace.jsx
@@ -20,6 +20,7 @@ import {
 } from "../hooks";
 import FloatingControls from "./FloatingControls";
 import { Button, Modal, Tag } from "@douyinfe/semi-ui";
+import { IconAlertTriangle } from "@douyinfe/semi-icons";
 import { useTranslation } from "react-i18next";
 import { databases } from "../data/databases";
 import { isRtl } from "../i18n/utils/rtl";
@@ -47,7 +48,7 @@ export default function WorkSpace() {
   const [showSelectDbModal, setShowSelectDbModal] = useState(false);
   const [showRestoreModal, setShowRestoreModal] = useState(false);
   const [selectedDb, setSelectedDb] = useState("");
-  const { layout } = useLayout();
+  const { layout, setLayout } = useLayout();
   const { settings } = useSettings();
   const { types, setTypes } = useTypes();
   const { areas, setAreas } = useAreas();
@@ -453,7 +454,7 @@ export default function WorkSpace() {
               <Button
                 icon={<i className="fa-solid fa-rotate-right mt-0.5"></i>}
               >
-                Restore version
+                {t("restore_version")}
               </Button>
             </div>
           )}
@@ -518,7 +519,21 @@ export default function WorkSpace() {
         centered
         closable
         onCancel={() => setShowRestoreModal(false)}
-      ></Modal>
+        title={
+          <span className="flex items-center gap-2">
+            <IconAlertTriangle className="text-amber-400" size="extra-large" /> {t("restore_version")}
+          </span>
+        }
+        okText={t("continue")}
+        cancelText={t("cancel")}
+        onOk={() => {
+          setLayout((prev) => ({ ...prev, readOnly: false }));
+          setShowRestoreModal(false);
+          setVersion(null);
+        }}
+      >
+        {t("restore_warning")}
+      </Modal>
     </div>
   );
 }
diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js
index b387b8603..8028ccf91 100644
--- a/src/i18n/locales/en.js
+++ b/src/i18n/locales/en.js
@@ -262,6 +262,10 @@ const en = {
     version: "Version",
     record_version: "Record version",
     commited_at: "Commited at",
+    read_only: "Read only",
+    continue: "Continue",
+    restore_version: "Restore version",
+    restore_warning: "Loading another version will overwrite the current changes."
   },
 };
 

From 87ba7608e82a29fd27bfef7b417d6bfe2e14b74e Mon Sep 17 00:00:00 2001
From: 1ilit <1ilit@proton.me>
Date: Sun, 27 Jul 2025 16:12:27 +0400
Subject: [PATCH 9/9] separate share and versions

---
 src/api/gists.js                              |  20 +-
 src/components/EditorHeader/ControlPanel.jsx  |   2 +
 src/components/EditorHeader/Modal/Share.jsx   |  11 +-
 .../EditorHeader/SideSheet/Revisions.jsx      | 174 +++++++++++++-----
 .../EditorHeader/SideSheet/Sidesheet.jsx      |  10 +-
 src/components/Workspace.jsx                  |  26 ++-
 src/i18n/locales/en.js                        |   3 +-
 7 files changed, 181 insertions(+), 65 deletions(-)

diff --git a/src/api/gists.js b/src/api/gists.js
index 87c15e9d1..bac5f5acf 100644
--- a/src/api/gists.js
+++ b/src/api/gists.js
@@ -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;
+}
\ No newline at end of file
diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx
index f8485e90a..37e3df58d 100644
--- a/src/components/EditorHeader/ControlPanel.jsx
+++ b/src/components/EditorHeader/ControlPanel.jsx
@@ -1508,6 +1508,8 @@ export default function ControlPanel({
       />
       <Sidesheet
         type={sidesheet}
+        title={title}
+        setTitle={setTitle}
         onClose={() => setSidesheet(SIDESHEET.NONE)}
       />
     </>
diff --git a/src/components/EditorHeader/Modal/Share.jsx b/src/components/EditorHeader/Modal/Share.jsx
index 582669722..71346a7ec 100644
--- a/src/components/EditorHeader/Modal/Share.jsx
+++ b/src/components/EditorHeader/Modal/Share.jsx
@@ -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);
diff --git a/src/components/EditorHeader/SideSheet/Revisions.jsx b/src/components/EditorHeader/SideSheet/Revisions.jsx
index 4a312efc3..fc091f37b 100644
--- a/src/components/EditorHeader/SideSheet/Revisions.jsx
+++ b/src/components/EditorHeader/SideSheet/Revisions.jsx
@@ -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>
   );
diff --git a/src/components/EditorHeader/SideSheet/Sidesheet.jsx b/src/components/EditorHeader/SideSheet/Sidesheet.jsx
index f3784aad8..5bfc4695a 100644
--- a/src/components/EditorHeader/SideSheet/Sidesheet.jsx
+++ b/src/components/EditorHeader/SideSheet/Sidesheet.jsx
@@ -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;
     }
diff --git a/src/components/Workspace.jsx b/src/components/Workspace.jsx
index 873b83c80..089da2181 100644
--- a/src/components/Workspace.jsx
+++ b/src/components/Workspace.jsx
@@ -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")}
diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js
index 8028ccf91..3ad15ecb7 100644
--- a/src/i18n/locales/en.js
+++ b/src/i18n/locales/en.js
@@ -265,7 +265,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"
   },
 };
 
