add revisions sidesheet

This commit is contained in:
1ilit
2025-04-06 02:35:46 +04:00
parent 1367ef3745
commit d17c552424
8 changed files with 295 additions and 1060 deletions

1058
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.11",
"oracle-sql-parser": "^0.1.0",
"react": "^18.2.0",

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -0,0 +1,195 @@
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";
import {
IconPlus,
IconChevronRight,
IconChevronLeft,
} from "@douyinfe/semi-icons";
import {
create,
getCommitsWithFile,
getVersion,
patch,
VERSION_FILENAME,
} from "../../../api/gists";
import { DateTime } from "luxon";
import {
useAreas,
useDiagram,
useEnums,
useLayout,
useNotes,
useTransform,
useTypes,
} from "../../../hooks";
import { databases } from "../../../data/databases";
export default function Versions({ open, title, setTitle }) {
const { gistId, setVersion } = useContext(IdContext);
const { areas, setAreas } = useAreas();
const { setLayout } = useLayout();
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 [versions, setVersions] = 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 {
const version = await getVersion(gistId, sha);
setVersion(sha);
setLayout((prev) => ({ ...prev, readOnly: true }));
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,
database,
setNotes,
setTypes,
setEnums,
setTitle,
],
);
const getRevisions = useCallback(
async (gistId) => {
try {
setIsLoading(true);
const { data } = await getCommitsWithFile(gistId, VERSION_FILENAME);
setVersions(
data.filter((version) => version.change_status.total !== 0),
);
} catch (e) {
console.log(e);
Toast.error(t("oops_smth_went_wrong"));
} finally {
setIsLoading(false);
}
},
[t],
);
const recordVersion = async () => {
try {
if (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, 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={recordVersion}>
{t("record_version")}
</Button>
<IconButton icon={<IconChevronRight />} title="Next" />
</div>
{isLoading ? (
<div className="text-blue-500 text-center mt-3">
<Spin size="middle" />
<div>{t("loading")}</div>
</div>
) : (
<>
{(!gistId || !versions.length) && (
<div className="my-3">{t("no_saved_versions")}</div>
)}
{gistId && (
<div className="my-3 overflow-y-auto">
<Steps direction="vertical" type="basic">
{versions.map((r, i) => (
<Steps.Step
key={r.version}
onClick={() => loadVersion(r.version)}
title={
<div className="flex justify-between items-center w-full">
<span>{`${t("version")} ${versions.length - i}`}</span>
<Tag>{r.version.substring(0, 7)}</Tag>
</div>
}
description={`${t("commited_at")} ${DateTime.fromISO(
r.committed_at,
)
.setLocale(i18n.language)
.toLocaleString(DateTime.DATETIME_MED)}`}
icon={<i className="text-sm fa-solid fa-asterisk" />}
/>
))}
</Steps>
</div>
)}
</>
)}
</div>
);
}

View File

@@ -260,6 +260,8 @@ const en = {
many_side_label: "Many(n) side label",
revisions: "Revisions",
no_saved_revisions: "No saved revisions",
version: "Version",
record_version: "Record version",
},
};

View File

@@ -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;
}