mirror of
https://github.com/drawdb-io/drawdb.git
synced 2026-01-13 07:02:37 +08:00
Diagram versioning (#560)
* clean up sidesheet * clean up sharing * add revisions sidesheet * update getCommits and clean up * update date localization * load diagram in read only mode from previous version * disable input from control panel and popovers * add restore warning modal * separate share and versions * update versions * finalize versioning implementation, add pagination * fix package-lock.json * clear versions cache on flush storgae * disable menubar items when in read only mode * disable remaining fields in readonlt * suppress eslint only-export-components rule * show loading version progress
This commit is contained in:
@@ -6,7 +6,7 @@ module.exports = {
|
|||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
"plugin:react/jsx-runtime",
|
"plugin:react/jsx-runtime",
|
||||||
"plugin:react-hooks/recommended",
|
"plugin:react-hooks/recommended",
|
||||||
"prettier"
|
"prettier",
|
||||||
],
|
],
|
||||||
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
||||||
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||||
@@ -18,5 +18,6 @@ module.exports = {
|
|||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
"react/prop-types": 0,
|
"react/prop-types": 0,
|
||||||
|
"react-refresh/only-export-components": "off",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
3965
package-lock.json
generated
3965
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "client-vite",
|
"name": "drawdb",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lexical": "^0.12.5",
|
"lexical": "^0.12.5",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"luxon": "^3.7.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"node-sql-parser": "^5.3.11",
|
"node-sql-parser": "^5.3.11",
|
||||||
"oracle-sql-parser": "^0.1.0",
|
"oracle-sql-parser": "^0.1.0",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const filename = "share.json";
|
export const SHARE_FILENAME = "share.json";
|
||||||
const description = "drawDB diagram";
|
export const VERSION_FILENAME = "versionned.json";
|
||||||
|
|
||||||
|
const description = "drawDB diagram";
|
||||||
const baseUrl = import.meta.env.VITE_BACKEND_URL;
|
const baseUrl = import.meta.env.VITE_BACKEND_URL;
|
||||||
|
|
||||||
export async function create(content) {
|
export async function create(filename, content) {
|
||||||
const res = await axios.post(`${baseUrl}/gists`, {
|
const res = await axios.post(`${baseUrl}/gists`, {
|
||||||
public: false,
|
public: false,
|
||||||
filename,
|
filename,
|
||||||
@@ -16,11 +17,13 @@ export async function create(content) {
|
|||||||
return res.data.data.id;
|
return res.data.data.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function patch(gistId, content) {
|
export async function patch(gistId, filename, content) {
|
||||||
await axios.patch(`${baseUrl}/gists/${gistId}`, {
|
const { deleted } = await axios.patch(`${baseUrl}/gists/${gistId}`, {
|
||||||
filename,
|
filename,
|
||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function del(gistId) {
|
export async function del(gistId) {
|
||||||
@@ -32,3 +35,39 @@ export async function get(gistId) {
|
|||||||
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCommits(gistId, perPage = 20, page = 1) {
|
||||||
|
const res = await axios.get(`${baseUrl}/gists/${gistId}/commits`, {
|
||||||
|
params: {
|
||||||
|
per_page: perPage,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVersion(gistId, sha) {
|
||||||
|
const res = await axios.get(`${baseUrl}/gists/${gistId}/${sha}`);
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCommitsWithFile(
|
||||||
|
gistId,
|
||||||
|
file,
|
||||||
|
limit = 10,
|
||||||
|
cursor = null,
|
||||||
|
) {
|
||||||
|
const res = await axios.get(
|
||||||
|
`${baseUrl}/gists/${gistId}/file-versions/${file}`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
cursor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
@@ -179,6 +179,7 @@ export default function Area({
|
|||||||
backgroundColor: "#2F68ADB3",
|
backgroundColor: "#2F68ADB3",
|
||||||
}}
|
}}
|
||||||
onClick={lockUnlockArea}
|
onClick={lockUnlockArea}
|
||||||
|
disabled={layout.readOnly}
|
||||||
/>
|
/>
|
||||||
<Popover
|
<Popover
|
||||||
visible={areaIsOpen() && !layout.sidebar}
|
visible={areaIsOpen() && !layout.sidebar}
|
||||||
@@ -257,6 +258,7 @@ function EditPopoverContent({ data }) {
|
|||||||
const { updateArea, deleteArea } = useAreas();
|
const { updateArea, deleteArea } = useAreas();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { layout } = useLayout();
|
||||||
const initialColorRef = useRef(data.color);
|
const initialColorRef = useRef(data.color);
|
||||||
|
|
||||||
const handleColorPick = (color) => {
|
const handleColorPick = (color) => {
|
||||||
@@ -302,6 +304,7 @@ function EditPopoverContent({ data }) {
|
|||||||
value={data.name}
|
value={data.name}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
className="me-2"
|
className="me-2"
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(value) => updateArea(data.id, { name: value })}
|
onChange={(value) => updateArea(data.id, { name: value })}
|
||||||
onFocus={(e) => setEditField({ name: e.target.value })}
|
onFocus={(e) => setEditField({ name: e.target.value })}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
@@ -325,6 +328,7 @@ function EditPopoverContent({ data }) {
|
|||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
usePopover={true}
|
usePopover={true}
|
||||||
|
readOnly={layout.readOnly}
|
||||||
value={data.color}
|
value={data.color}
|
||||||
onChange={(color) => updateArea(data.id, { color })}
|
onChange={(color) => updateArea(data.id, { color })}
|
||||||
onColorPick={(color) => handleColorPick(color)}
|
onColorPick={(color) => handleColorPick(color)}
|
||||||
@@ -336,6 +340,7 @@ function EditPopoverContent({ data }) {
|
|||||||
type="danger"
|
type="danger"
|
||||||
block
|
block
|
||||||
onClick={() => deleteArea(data.id, true)}
|
onClick={() => deleteArea(data.id, true)}
|
||||||
|
disabled={layout.readOnly}
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ export default function Canvas() {
|
|||||||
|
|
||||||
if (!e.isPrimary) return;
|
if (!e.isPrimary) return;
|
||||||
|
|
||||||
|
|
||||||
if (panning.isPanning) {
|
if (panning.isPanning) {
|
||||||
setTransform((prev) => ({
|
setTransform((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -294,6 +295,8 @@ export default function Canvas() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(layout.readOnly) return;
|
||||||
|
|
||||||
if (linking) {
|
if (linking) {
|
||||||
setLinkingLine({
|
setLinkingLine({
|
||||||
...linkingLine,
|
...linkingLine,
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export default function Note({ data, onPointerDown }) {
|
|||||||
backgroundColor: "#2F68ADB3",
|
backgroundColor: "#2F68ADB3",
|
||||||
}}
|
}}
|
||||||
onClick={lockUnlockNote}
|
onClick={lockUnlockNote}
|
||||||
|
disabled={layout.readOnly}
|
||||||
/>
|
/>
|
||||||
<Popover
|
<Popover
|
||||||
visible={
|
visible={
|
||||||
@@ -280,6 +281,7 @@ export default function Note({ data, onPointerDown }) {
|
|||||||
value={data.title}
|
value={data.title}
|
||||||
placeholder={t("title")}
|
placeholder={t("title")}
|
||||||
className="me-2"
|
className="me-2"
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
updateNote(data.id, { title: value })
|
updateNote(data.id, { title: value })
|
||||||
}
|
}
|
||||||
@@ -307,6 +309,7 @@ export default function Note({ data, onPointerDown }) {
|
|||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
usePopover={true}
|
usePopover={true}
|
||||||
|
readOnly={layout.readOnly}
|
||||||
value={data.color}
|
value={data.color}
|
||||||
onChange={(color) => updateNote(data.id, { color })}
|
onChange={(color) => updateNote(data.id, { color })}
|
||||||
onColorPick={(color) => handleColorPick(color)}
|
onColorPick={(color) => handleColorPick(color)}
|
||||||
@@ -314,9 +317,10 @@ export default function Note({ data, onPointerDown }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDeleteStroked />}
|
|
||||||
type="danger"
|
|
||||||
block
|
block
|
||||||
|
type="danger"
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
onClick={() => deleteNote(data.id, true)}
|
onClick={() => deleteNote(data.id, true)}
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
@@ -343,6 +347,7 @@ export default function Note({ data, onPointerDown }) {
|
|||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
id={`note_${data.id}`}
|
id={`note_${data.id}`}
|
||||||
|
readOnly={layout.readOnly}
|
||||||
value={data.content}
|
value={data.content}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={(e) =>
|
onFocus={(e) =>
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ export default function Table({
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: "#2f68adb3",
|
backgroundColor: "#2f68adb3",
|
||||||
}}
|
}}
|
||||||
|
disabled={layout.readOnly}
|
||||||
onClick={lockUnlockTable}
|
onClick={lockUnlockTable}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -234,6 +235,7 @@ export default function Table({
|
|||||||
block
|
block
|
||||||
style={{ marginTop: "8px" }}
|
style={{ marginTop: "8px" }}
|
||||||
onClick={() => deleteTable(tableData.id)}
|
onClick={() => deleteTable(tableData.id)}
|
||||||
|
disabled={layout.readOnly}
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export default function ControlPanel({
|
|||||||
const { selectedElement, setSelectedElement } = useSelect();
|
const { selectedElement, setSelectedElement } = useSelect();
|
||||||
const { transform, setTransform } = useTransform();
|
const { transform, setTransform } = useTransform();
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { setGistId } = useContext(IdContext);
|
const { version, setGistId } = useContext(IdContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const invertLayout = (component) =>
|
const invertLayout = (component) =>
|
||||||
@@ -615,6 +615,9 @@ export default function ControlPanel({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const del = () => {
|
const del = () => {
|
||||||
|
if (layout.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (selectedElement.element) {
|
switch (selectedElement.element) {
|
||||||
case ObjectType.TABLE:
|
case ObjectType.TABLE:
|
||||||
deleteTable(selectedElement.id);
|
deleteTable(selectedElement.id);
|
||||||
@@ -630,6 +633,9 @@ export default function ControlPanel({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const duplicate = () => {
|
const duplicate = () => {
|
||||||
|
if (layout.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (selectedElement.element) {
|
switch (selectedElement.element) {
|
||||||
case ObjectType.TABLE: {
|
case ObjectType.TABLE: {
|
||||||
const copiedTable = tables.find((t) => t.id === selectedElement.id);
|
const copiedTable = tables.find((t) => t.id === selectedElement.id);
|
||||||
@@ -685,6 +691,9 @@ export default function ControlPanel({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const paste = () => {
|
const paste = () => {
|
||||||
|
if (layout.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
navigator.clipboard.readText().then((text) => {
|
navigator.clipboard.readText().then((text) => {
|
||||||
let obj = null;
|
let obj = null;
|
||||||
try {
|
try {
|
||||||
@@ -718,6 +727,9 @@ export default function ControlPanel({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const cut = () => {
|
const cut = () => {
|
||||||
|
if (layout.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
copy();
|
copy();
|
||||||
del();
|
del();
|
||||||
};
|
};
|
||||||
@@ -747,10 +759,12 @@ export default function ControlPanel({
|
|||||||
save: {
|
save: {
|
||||||
function: save,
|
function: save,
|
||||||
shortcut: "Ctrl+S",
|
shortcut: "Ctrl+S",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
save_as: {
|
save_as: {
|
||||||
function: saveDiagramAs,
|
function: saveDiagramAs,
|
||||||
shortcut: "Ctrl+Shift+S",
|
shortcut: "Ctrl+Shift+S",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
save_as_template: {
|
save_as_template: {
|
||||||
function: () => {
|
function: () => {
|
||||||
@@ -775,6 +789,7 @@ export default function ControlPanel({
|
|||||||
function: () => {
|
function: () => {
|
||||||
setModal(MODAL.RENAME);
|
setModal(MODAL.RENAME);
|
||||||
},
|
},
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
delete_diagram: {
|
delete_diagram: {
|
||||||
warning: {
|
warning: {
|
||||||
@@ -805,6 +820,7 @@ export default function ControlPanel({
|
|||||||
{
|
{
|
||||||
function: fileImport,
|
function: fileImport,
|
||||||
name: "JSON",
|
name: "JSON",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
function: () => {
|
function: () => {
|
||||||
@@ -812,6 +828,7 @@ export default function ControlPanel({
|
|||||||
setImportFrom(IMPORT_FROM.DBML);
|
setImportFrom(IMPORT_FROM.DBML);
|
||||||
},
|
},
|
||||||
name: "DBML",
|
name: "DBML",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -824,6 +841,7 @@ export default function ControlPanel({
|
|||||||
setImportDb(DB.MYSQL);
|
setImportDb(DB.MYSQL);
|
||||||
},
|
},
|
||||||
name: "MySQL",
|
name: "MySQL",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
function: () => {
|
function: () => {
|
||||||
@@ -831,6 +849,7 @@ export default function ControlPanel({
|
|||||||
setImportDb(DB.POSTGRES);
|
setImportDb(DB.POSTGRES);
|
||||||
},
|
},
|
||||||
name: "PostgreSQL",
|
name: "PostgreSQL",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
function: () => {
|
function: () => {
|
||||||
@@ -838,6 +857,7 @@ export default function ControlPanel({
|
|||||||
setImportDb(DB.SQLITE);
|
setImportDb(DB.SQLITE);
|
||||||
},
|
},
|
||||||
name: "SQLite",
|
name: "SQLite",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
function: () => {
|
function: () => {
|
||||||
@@ -845,6 +865,7 @@ export default function ControlPanel({
|
|||||||
setImportDb(DB.MARIADB);
|
setImportDb(DB.MARIADB);
|
||||||
},
|
},
|
||||||
name: "MariaDB",
|
name: "MariaDB",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
function: () => {
|
function: () => {
|
||||||
@@ -852,6 +873,7 @@ export default function ControlPanel({
|
|||||||
setImportDb(DB.MSSQL);
|
setImportDb(DB.MSSQL);
|
||||||
},
|
},
|
||||||
name: "MSSQL",
|
name: "MSSQL",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
function: () => {
|
function: () => {
|
||||||
@@ -860,6 +882,7 @@ export default function ControlPanel({
|
|||||||
},
|
},
|
||||||
name: "Oracle",
|
name: "Oracle",
|
||||||
label: "Beta",
|
label: "Beta",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -868,6 +891,7 @@ export default function ControlPanel({
|
|||||||
|
|
||||||
setModal(MODAL.IMPORT_SRC);
|
setModal(MODAL.IMPORT_SRC);
|
||||||
},
|
},
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
export_source: {
|
export_source: {
|
||||||
...(database === DB.GENERIC && {
|
...(database === DB.GENERIC && {
|
||||||
@@ -1159,10 +1183,12 @@ export default function ControlPanel({
|
|||||||
undo: {
|
undo: {
|
||||||
function: undo,
|
function: undo,
|
||||||
shortcut: "Ctrl+Z",
|
shortcut: "Ctrl+Z",
|
||||||
|
disabled: layout.readOnly || undoStack.length === 0,
|
||||||
},
|
},
|
||||||
redo: {
|
redo: {
|
||||||
function: redo,
|
function: redo,
|
||||||
shortcut: "Ctrl+Y",
|
shortcut: "Ctrl+Y",
|
||||||
|
disabled: layout.readOnly || redoStack.length === 0,
|
||||||
},
|
},
|
||||||
clear: {
|
clear: {
|
||||||
warning: {
|
warning: {
|
||||||
@@ -1194,14 +1220,17 @@ export default function ControlPanel({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
function: edit,
|
function: edit,
|
||||||
shortcut: "Ctrl+E",
|
shortcut: "Ctrl+E",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
cut: {
|
cut: {
|
||||||
function: cut,
|
function: cut,
|
||||||
shortcut: "Ctrl+X",
|
shortcut: "Ctrl+X",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
function: copy,
|
function: copy,
|
||||||
@@ -1210,14 +1239,17 @@ export default function ControlPanel({
|
|||||||
paste: {
|
paste: {
|
||||||
function: paste,
|
function: paste,
|
||||||
shortcut: "Ctrl+V",
|
shortcut: "Ctrl+V",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
duplicate: {
|
duplicate: {
|
||||||
function: duplicate,
|
function: duplicate,
|
||||||
shortcut: "Ctrl+D",
|
shortcut: "Ctrl+D",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
function: del,
|
function: del,
|
||||||
shortcut: "Del",
|
shortcut: "Del",
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
copy_as_image: {
|
copy_as_image: {
|
||||||
function: copyAsImage,
|
function: copyAsImage,
|
||||||
@@ -1404,6 +1436,7 @@ export default function ControlPanel({
|
|||||||
},
|
},
|
||||||
table_width: {
|
table_width: {
|
||||||
function: () => setModal(MODAL.TABLE_WIDTH),
|
function: () => setModal(MODAL.TABLE_WIDTH),
|
||||||
|
disabled: layout.readOnly,
|
||||||
},
|
},
|
||||||
language: {
|
language: {
|
||||||
function: () => setModal(MODAL.LANGUAGE),
|
function: () => setModal(MODAL.LANGUAGE),
|
||||||
@@ -1417,6 +1450,7 @@ export default function ControlPanel({
|
|||||||
message: t("are_you_sure_flush_storage"),
|
message: t("are_you_sure_flush_storage"),
|
||||||
},
|
},
|
||||||
function: async () => {
|
function: async () => {
|
||||||
|
localStorage.removeItem("versions_cache");
|
||||||
db.delete()
|
db.delete()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Toast.success(t("storage_flushed"));
|
Toast.success(t("storage_flushed"));
|
||||||
@@ -1513,6 +1547,8 @@ export default function ControlPanel({
|
|||||||
/>
|
/>
|
||||||
<Sidesheet
|
<Sidesheet
|
||||||
type={sidesheet}
|
type={sidesheet}
|
||||||
|
title={title}
|
||||||
|
setTitle={setTitle}
|
||||||
onClose={() => setSidesheet(SIDESHEET.NONE)}
|
onClose={() => setSidesheet(SIDESHEET.NONE)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -1603,47 +1639,46 @@ export default function ControlPanel({
|
|||||||
<Divider layout="vertical" margin="8px" />
|
<Divider layout="vertical" margin="8px" />
|
||||||
<Tooltip content={t("undo")} position="bottom">
|
<Tooltip content={t("undo")} position="bottom">
|
||||||
<button
|
<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 || layout.readOnly}
|
||||||
onClick={undo}
|
onClick={undo}
|
||||||
>
|
>
|
||||||
<IconUndo
|
<IconUndo size="large" />
|
||||||
size="large"
|
|
||||||
style={{ color: undoStack.length === 0 ? "#9598a6" : "" }}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={t("redo")} position="bottom">
|
<Tooltip content={t("redo")} position="bottom">
|
||||||
<button
|
<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 || layout.readOnly}
|
||||||
onClick={redo}
|
onClick={redo}
|
||||||
>
|
>
|
||||||
<IconRedo
|
<IconRedo size="large" />
|
||||||
size="large"
|
|
||||||
style={{ color: redoStack.length === 0 ? "#9598a6" : "" }}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Divider layout="vertical" margin="8px" />
|
<Divider layout="vertical" margin="8px" />
|
||||||
<Tooltip content={t("add_table")} position="bottom">
|
<Tooltip content={t("add_table")} position="bottom">
|
||||||
<button
|
<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()}
|
onClick={() => addTable()}
|
||||||
|
disabled={layout.readOnly}
|
||||||
>
|
>
|
||||||
<IconAddTable />
|
<IconAddTable />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={t("add_area")} position="bottom">
|
<Tooltip content={t("add_area")} position="bottom">
|
||||||
<button
|
<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()}
|
onClick={() => addArea()}
|
||||||
|
disabled={layout.readOnly}
|
||||||
>
|
>
|
||||||
<IconAddArea />
|
<IconAddArea />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={t("add_note")} position="bottom">
|
<Tooltip content={t("add_note")} position="bottom">
|
||||||
<button
|
<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()}
|
onClick={() => addNote()}
|
||||||
|
disabled={layout.readOnly}
|
||||||
>
|
>
|
||||||
<IconAddNote />
|
<IconAddNote />
|
||||||
</button>
|
</button>
|
||||||
@@ -1651,12 +1686,21 @@ export default function ControlPanel({
|
|||||||
<Divider layout="vertical" margin="8px" />
|
<Divider layout="vertical" margin="8px" />
|
||||||
<Tooltip content={t("save")} position="bottom">
|
<Tooltip content={t("save")} position="bottom">
|
||||||
<button
|
<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}
|
onClick={save}
|
||||||
|
disabled={layout.readOnly}
|
||||||
>
|
>
|
||||||
<IconSaveStroked size="extra-large" />
|
<IconSaveStroked size="extra-large" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip content={t("versions")} position="bottom">
|
||||||
|
<button
|
||||||
|
className="py-1 px-2 hover-2 rounded-sm text-xl -mt-0.5"
|
||||||
|
onClick={() => setSidesheet(SIDESHEET.VERSIONS)}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-code-branch" />{" "}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip content={t("to_do")} position="bottom">
|
<Tooltip content={t("to_do")} position="bottom">
|
||||||
<button
|
<button
|
||||||
className="py-1 px-2 hover-2 rounded-sm text-xl -mt-0.5"
|
className="py-1 px-2 hover-2 rounded-sm text-xl -mt-0.5"
|
||||||
@@ -1743,7 +1787,7 @@ export default function ControlPanel({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="text-xl me-1"
|
className="text-xl flex items-center gap-1 me-1"
|
||||||
onPointerEnter={(e) => e.isPrimary && setShowEditName(true)}
|
onPointerEnter={(e) => e.isPrimary && setShowEditName(true)}
|
||||||
onPointerLeave={(e) => e.isPrimary && setShowEditName(false)}
|
onPointerLeave={(e) => e.isPrimary && setShowEditName(false)}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
@@ -1751,14 +1795,24 @@ export default function ControlPanel({
|
|||||||
// https://stackoverflow.com/a/70976017/1137077
|
// https://stackoverflow.com/a/70976017/1137077
|
||||||
e.target.releasePointerCapture(e.pointerId);
|
e.target.releasePointerCapture(e.pointerId);
|
||||||
}}
|
}}
|
||||||
onClick={() => setModal(MODAL.RENAME)}
|
onClick={!layout.readOnly && (() => setModal(MODAL.RENAME))}
|
||||||
>
|
>
|
||||||
{window.name.split(" ")[0] === "t" ? "Templates/" : "Diagrams/"}
|
<span>
|
||||||
{title}
|
{(window.name.split(" ")[0] === "t"
|
||||||
|
? "Templates/"
|
||||||
|
: "Diagrams/") + title}
|
||||||
|
</span>
|
||||||
|
{version && (
|
||||||
|
<Tag className="mt-1" color="blue" size="small">
|
||||||
|
{version.substring(0, 7)}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(showEditName || modal === MODAL.RENAME) && <IconEdit />}
|
{(showEditName || modal === MODAL.RENAME) && !layout.readOnly && (
|
||||||
|
<IconEdit />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex justify-start text-md select-none me-2">
|
<div className="flex justify-start text-md select-none me-2">
|
||||||
{Object.keys(menu).map((category) => (
|
{Object.keys(menu).map((category) => (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -1785,6 +1839,7 @@ export default function ControlPanel({
|
|||||||
key={i}
|
key={i}
|
||||||
onClick={e.function}
|
onClick={e.function}
|
||||||
className="flex justify-between"
|
className="flex justify-between"
|
||||||
|
disabled={e.disabled}
|
||||||
>
|
>
|
||||||
<span>{e.name}</span>
|
<span>{e.name}</span>
|
||||||
{e.label && (
|
{e.label && (
|
||||||
@@ -1838,6 +1893,7 @@ export default function ControlPanel({
|
|||||||
return (
|
return (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key={index}
|
key={index}
|
||||||
|
disabled={menu[category][item].disabled}
|
||||||
onClick={menu[category][item].function}
|
onClick={menu[category][item].function}
|
||||||
style={
|
style={
|
||||||
menu[category][item].shortcut && {
|
menu[category][item].shortcut && {
|
||||||
@@ -1871,17 +1927,21 @@ export default function ControlPanel({
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{layout.readOnly && <Tag size="small">{t("read_only")}</Tag>}
|
||||||
|
{!layout.readOnly && (
|
||||||
|
<Tag
|
||||||
size="small"
|
size="small"
|
||||||
type="tertiary"
|
type="light"
|
||||||
icon={
|
prefixIcon={
|
||||||
saveState === State.LOADING || saveState === State.SAVING ? (
|
saveState === State.LOADING ||
|
||||||
|
saveState === State.SAVING ? (
|
||||||
<Spin size="small" />
|
<Spin size="small" />
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{getState()}
|
{getState()}
|
||||||
</Button>
|
</Tag>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { Input } from "@douyinfe/semi-ui";
|
import { Input } from "@douyinfe/semi-ui";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useLayout } from "../../../hooks";
|
||||||
|
|
||||||
export default function Rename({ title, setTitle }) {
|
export default function Rename({ title, setTitle }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { layout } = useLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
defaultValue={title}
|
defaultValue={title}
|
||||||
onChange={(v) => setTitle(v)}
|
onChange={(v) => setTitle(v)}
|
||||||
|
readonly={layout.readOnly}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { InputNumber } from "@douyinfe/semi-ui";
|
import { InputNumber } from "@douyinfe/semi-ui";
|
||||||
import { useSettings } from "../../../hooks";
|
import { useLayout, useSettings } from "../../../hooks";
|
||||||
|
|
||||||
export default function SetTableWidth() {
|
export default function SetTableWidth() {
|
||||||
|
const { layout } = useLayout();
|
||||||
const { settings, setSettings } = useSettings();
|
const { settings, setSettings } = useSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputNumber
|
<InputNumber
|
||||||
className="w-full"
|
className="w-full"
|
||||||
value={settings.tableWidth}
|
value={settings.tableWidth}
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(c) => {
|
onChange={(c) => {
|
||||||
if (c < 180) return;
|
if (c < 180) return;
|
||||||
setSettings((prev) => ({ ...prev, tableWidth: c }));
|
setSettings((prev) => ({ ...prev, tableWidth: c }));
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from "../../../hooks";
|
} from "../../../hooks";
|
||||||
import { databases } from "../../../data/databases";
|
import { databases } from "../../../data/databases";
|
||||||
import { MODAL } from "../../../data/constants";
|
import { MODAL } from "../../../data/constants";
|
||||||
import { create, del, patch } from "../../../api/gists";
|
import { create, patch, SHARE_FILENAME } from "../../../api/gists";
|
||||||
|
|
||||||
export default function Share({ title, setModal }) {
|
export default function Share({ title, setModal }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -55,24 +55,26 @@ export default function Share({ title, setModal }) {
|
|||||||
|
|
||||||
const unshare = useCallback(async () => {
|
const unshare = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await del(gistId);
|
const deleted = await patch(gistId, SHARE_FILENAME, undefined);
|
||||||
|
if (deleted) {
|
||||||
setGistId("");
|
setGistId("");
|
||||||
|
}
|
||||||
setModal(MODAL.NONE);
|
setModal(MODAL.NONE);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(e);
|
setError(e);
|
||||||
}
|
}
|
||||||
}, [gistId, setGistId, setModal]);
|
}, [gistId, setModal, setGistId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateOrGenerateLink = async () => {
|
const updateOrGenerateLink = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (!gistId || gistId === "") {
|
if (!gistId || gistId === "") {
|
||||||
const id = await create(diagramToString());
|
const id = await create(SHARE_FILENAME, diagramToString());
|
||||||
setGistId(id);
|
setGistId(id);
|
||||||
} else {
|
} else {
|
||||||
await patch(gistId, diagramToString());
|
await patch(gistId, SHARE_FILENAME, diagramToString());
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -116,7 +118,7 @@ export default function Share({ title, setModal }) {
|
|||||||
{!error && (
|
{!error && (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Input value={url} size="large" />
|
<Input value={url} size="large" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs mt-2">{t("share_info")}</div>
|
<div className="text-xs mt-2">{t("share_info")}</div>
|
||||||
<div className="flex gap-2 mt-3">
|
<div className="flex gap-2 mt-3">
|
||||||
|
|||||||
@@ -1,37 +1,21 @@
|
|||||||
import { SideSheet as SemiUISideSheet } from "@douyinfe/semi-ui";
|
import { SideSheet as SemiUISideSheet } from "@douyinfe/semi-ui";
|
||||||
import { SIDESHEET } from "../../../data/constants";
|
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 Timeline from "./Timeline";
|
||||||
import Todo from "./Todo";
|
import Todo from "./Todo";
|
||||||
|
import Versions from "./Versions";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function Sidesheet({ type, onClose }) {
|
export default function Sidesheet({ type, title, setTitle, onClose }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { settings } = useSettings();
|
|
||||||
|
|
||||||
function getTitle(type) {
|
function getTitle(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SIDESHEET.TIMELINE:
|
case SIDESHEET.TIMELINE:
|
||||||
return (
|
return t("timeline");
|
||||||
<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>
|
|
||||||
);
|
|
||||||
case SIDESHEET.TODO:
|
case SIDESHEET.TODO:
|
||||||
return (
|
return t("to_do");
|
||||||
<div className="flex items-center">
|
case SIDESHEET.VERSIONS:
|
||||||
<img src={todo} className="w-7" alt="todo icon" />
|
return t("versions");
|
||||||
<div className="ms-3 text-lg">{t("to_do")}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -43,6 +27,14 @@ export default function Sidesheet({ type, onClose }) {
|
|||||||
return <Timeline />;
|
return <Timeline />;
|
||||||
case SIDESHEET.TODO:
|
case SIDESHEET.TODO:
|
||||||
return <Todo />;
|
return <Todo />;
|
||||||
|
case SIDESHEET.VERSIONS:
|
||||||
|
return (
|
||||||
|
<Versions
|
||||||
|
open={type !== SIDESHEET.NONE}
|
||||||
|
title={title}
|
||||||
|
setTitle={setTitle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -52,12 +44,12 @@ export default function Sidesheet({ type, onClose }) {
|
|||||||
<SemiUISideSheet
|
<SemiUISideSheet
|
||||||
visible={type !== SIDESHEET.NONE}
|
visible={type !== SIDESHEET.NONE}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
width={340}
|
width={420}
|
||||||
title={getTitle(type)}
|
title={<div className="text-lg">{getTitle(type)}</div>}
|
||||||
style={{ paddingBottom: "16px" }}
|
style={{ paddingBottom: "16px" }}
|
||||||
bodyStyle={{ padding: "0px" }}
|
bodyStyle={{ padding: "0px" }}
|
||||||
>
|
>
|
||||||
{getContent(type)}
|
<div className="sidesheet-theme">{getContent(type)}</div>
|
||||||
</SemiUISideSheet>
|
</SemiUISideSheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function Timeline() {
|
|||||||
|
|
||||||
if (undoStack.length > 0) {
|
if (undoStack.length > 0) {
|
||||||
return (
|
return (
|
||||||
<List className="sidesheet-theme">
|
<List>
|
||||||
{[...undoStack].reverse().map((e, i) => (
|
{[...undoStack].reverse().map((e, i) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
key={i}
|
key={i}
|
||||||
@@ -24,6 +24,6 @@ export default function Timeline() {
|
|||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <div className="m-5 sidesheet-theme">{t("no_activity")}</div>;
|
return <div className="m-5">{t("no_activity")}</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function Todo() {
|
|||||||
|
|
||||||
return (
|
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
|
<Dropdown
|
||||||
render={
|
render={
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
@@ -153,7 +153,7 @@ export default function Todo() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{tasks.length > 0 ? (
|
{tasks.length > 0 ? (
|
||||||
<List className="sidesheet-theme">
|
<List>
|
||||||
{tasks.map((task, i) => (
|
{tasks.map((task, i) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
key={i}
|
key={i}
|
||||||
@@ -267,7 +267,7 @@ export default function Todo() {
|
|||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
) : (
|
) : (
|
||||||
<div className="m-5 sidesheet-theme">{t("no_tasks")}</div>
|
<div className="m-5">{t("no_tasks")}</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
320
src/components/EditorHeader/SideSheet/Versions.jsx
Normal file
320
src/components/EditorHeader/SideSheet/Versions.jsx
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
import { useCallback, useContext, useEffect, useState, useMemo } from "react";
|
||||||
|
import { IdContext } from "../../Workspace";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button, Spin, Steps, Tag, Toast } from "@douyinfe/semi-ui";
|
||||||
|
import { IconPlus } from "@douyinfe/semi-icons";
|
||||||
|
import {
|
||||||
|
create,
|
||||||
|
getCommitsWithFile,
|
||||||
|
getVersion,
|
||||||
|
patch,
|
||||||
|
get,
|
||||||
|
VERSION_FILENAME,
|
||||||
|
} from "../../../api/gists";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
import {
|
||||||
|
useAreas,
|
||||||
|
useDiagram,
|
||||||
|
useEnums,
|
||||||
|
useLayout,
|
||||||
|
useNotes,
|
||||||
|
useTransform,
|
||||||
|
useTypes,
|
||||||
|
} from "../../../hooks";
|
||||||
|
import { databases } from "../../../data/databases";
|
||||||
|
|
||||||
|
const LIMIT = 10;
|
||||||
|
const STORAGE_KEY = "versions_cache";
|
||||||
|
|
||||||
|
function loadCache() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
return saved ? JSON.parse(saved) : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCache(cache) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(cache));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Versions({ open, title, setTitle }) {
|
||||||
|
const { gistId, setGistId, version, 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 [hasMore, setHasMore] = useState(false);
|
||||||
|
const [cursor, setCursor] = useState(null);
|
||||||
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
|
const [loadingVersion, setLoadingVersion] = useState(null);
|
||||||
|
|
||||||
|
const cacheRef = useMemo(() => loadCache(), []);
|
||||||
|
|
||||||
|
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 currentStep = useMemo(() => {
|
||||||
|
if (!version) return 0;
|
||||||
|
return versions.findIndex((v) => v.version === version);
|
||||||
|
}, [version, versions]);
|
||||||
|
|
||||||
|
const loadVersion = useCallback(
|
||||||
|
async (sha) => {
|
||||||
|
try {
|
||||||
|
setLoadingVersion(sha);
|
||||||
|
const version = await getVersion(gistId, sha);
|
||||||
|
setVersion(sha);
|
||||||
|
setLayout((prev) => ({ ...prev, readOnly: true }));
|
||||||
|
|
||||||
|
if (!version.data.files[VERSION_FILENAME]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
Toast.error(t("failed_to_load_diagram"));
|
||||||
|
} finally {
|
||||||
|
setLoadingVersion(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
t,
|
||||||
|
gistId,
|
||||||
|
setTables,
|
||||||
|
setRelationships,
|
||||||
|
setAreas,
|
||||||
|
setVersion,
|
||||||
|
setLayout,
|
||||||
|
database,
|
||||||
|
setNotes,
|
||||||
|
setTypes,
|
||||||
|
setEnums,
|
||||||
|
setTitle,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRevisions = useCallback(
|
||||||
|
async (cursorParam) => {
|
||||||
|
try {
|
||||||
|
if (!gistId) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const cached = cacheRef[gistId];
|
||||||
|
if (cached && !cursorParam) {
|
||||||
|
setVersions(cached.versions);
|
||||||
|
setCursor(cached.cursor);
|
||||||
|
setHasMore(cached.hasMore);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await getCommitsWithFile(
|
||||||
|
gistId,
|
||||||
|
VERSION_FILENAME,
|
||||||
|
LIMIT,
|
||||||
|
cursorParam,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newVersions = cursorParam ? [...versions, ...res.data] : res.data;
|
||||||
|
|
||||||
|
setVersions(newVersions);
|
||||||
|
setHasMore(res.pagination.hasMore);
|
||||||
|
setCursor(res.pagination.cursor);
|
||||||
|
|
||||||
|
cacheRef[gistId] = {
|
||||||
|
versions: newVersions,
|
||||||
|
cursor: res.pagination.cursor,
|
||||||
|
hasMore: res.pagination.hasMore,
|
||||||
|
};
|
||||||
|
saveCache(cacheRef);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(t("oops_smth_went_wrong"));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[gistId, versions, t, cacheRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasDiagramChanged = async () => {
|
||||||
|
if (!gistId) return true;
|
||||||
|
|
||||||
|
const previousVersion = await get(gistId);
|
||||||
|
|
||||||
|
if (!previousVersion.data.files[VERSION_FILENAME]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousDiagram = JSON.parse(
|
||||||
|
previousVersion.data.files[VERSION_FILENAME]?.content,
|
||||||
|
);
|
||||||
|
const currentDiagram = {
|
||||||
|
title,
|
||||||
|
tables,
|
||||||
|
relationships: relationships,
|
||||||
|
notes: notes,
|
||||||
|
subjectAreas: areas,
|
||||||
|
database: database,
|
||||||
|
...(databases[database].hasTypes && { types: types }),
|
||||||
|
...(databases[database].hasEnums && { enums: enums }),
|
||||||
|
transform: transform,
|
||||||
|
};
|
||||||
|
|
||||||
|
return !_.isEqual(previousDiagram, currentDiagram);
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordVersion = async () => {
|
||||||
|
try {
|
||||||
|
setIsRecording(true);
|
||||||
|
const hasChanges = await hasDiagramChanged();
|
||||||
|
if (!hasChanges) {
|
||||||
|
Toast.info(t("no_changes_to_record"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gistId) {
|
||||||
|
await patch(gistId, VERSION_FILENAME, diagramToString());
|
||||||
|
} else {
|
||||||
|
const id = await create(VERSION_FILENAME, diagramToString());
|
||||||
|
setGistId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete cacheRef[gistId];
|
||||||
|
saveCache(cacheRef);
|
||||||
|
|
||||||
|
await getRevisions();
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(t("failed_to_record_version"));
|
||||||
|
} finally {
|
||||||
|
setIsRecording(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClearCache = () => {
|
||||||
|
delete cacheRef[gistId];
|
||||||
|
saveCache(cacheRef);
|
||||||
|
Toast.success(t("cache_cleared"));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gistId && open) {
|
||||||
|
getRevisions();
|
||||||
|
}
|
||||||
|
}, [gistId, open, getRevisions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-5 relative h-full">
|
||||||
|
<div className="sticky top-0 z-10 sidesheet-theme pb-2 grid grid-cols-3 gap-2">
|
||||||
|
<Button
|
||||||
|
className={cacheRef[gistId] ? "col-span-2" : "col-span-3"}
|
||||||
|
block
|
||||||
|
icon={isRecording ? <Spin /> : <IconPlus />}
|
||||||
|
disabled={isLoading || isRecording}
|
||||||
|
onClick={recordVersion}
|
||||||
|
>
|
||||||
|
{t("record_version")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{cacheRef[gistId] && (
|
||||||
|
<Button block type="danger" onClick={onClearCache}>
|
||||||
|
{t("clear_cache")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(!gistId || !versions.length) && !isLoading && (
|
||||||
|
<div className="my-3">{t("no_saved_versions")}</div>
|
||||||
|
)}
|
||||||
|
{gistId && (
|
||||||
|
<div className="my-3 overflow-y-auto">
|
||||||
|
<Steps direction="vertical" type="basic" current={currentStep}>
|
||||||
|
{versions.map((r) => (
|
||||||
|
<Steps.Step
|
||||||
|
key={r.version}
|
||||||
|
onClick={() => loadVersion(r.version)}
|
||||||
|
className="group"
|
||||||
|
title={
|
||||||
|
<div className="flex justify-between items-center w-full">
|
||||||
|
<Tag>{r.version.substring(0, 7)}</Tag>
|
||||||
|
<span className="text-xs hidden group-hover:inline-block">
|
||||||
|
{t("click_to_view")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
description={`${t("commited_at")} ${DateTime.fromISO(
|
||||||
|
r.committed_at,
|
||||||
|
)
|
||||||
|
.setLocale(i18n.language)
|
||||||
|
.toLocaleString(DateTime.DATETIME_MED)}`}
|
||||||
|
icon={
|
||||||
|
r.version === loadingVersion ? (
|
||||||
|
<Spin size="small" />
|
||||||
|
) : (
|
||||||
|
<i className="text-sm fa-solid fa-asterisk ms-1" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Steps>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLoading && !isRecording && (
|
||||||
|
<div className="text-blue-500 text-center my-3">
|
||||||
|
<Spin size="middle" />
|
||||||
|
<div>{t("loading")}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasMore && !isLoading && (
|
||||||
|
<div className="text-center">
|
||||||
|
<Button onClick={() => getRevisions(cursor)}>{t("load_more")}</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,12 +2,13 @@ import { useState, useRef } from "react";
|
|||||||
import { Button, Input } from "@douyinfe/semi-ui";
|
import { Button, Input } from "@douyinfe/semi-ui";
|
||||||
import ColorPicker from "../ColorPicker";
|
import ColorPicker from "../ColorPicker";
|
||||||
import { IconDeleteStroked } from "@douyinfe/semi-icons";
|
import { IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||||
import { useAreas, useUndoRedo } from "../../../hooks";
|
import { useAreas, useLayout, useUndoRedo } from "../../../hooks";
|
||||||
import { Action, ObjectType } from "../../../data/constants";
|
import { Action, ObjectType } from "../../../data/constants";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function AreaInfo({ data, i }) {
|
export default function AreaInfo({ data, i }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { deleteArea, updateArea } = useAreas();
|
const { deleteArea, updateArea } = useAreas();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const [editField, setEditField] = useState({});
|
const [editField, setEditField] = useState({});
|
||||||
@@ -53,6 +54,7 @@ export default function AreaInfo({ data, i }) {
|
|||||||
<Input
|
<Input
|
||||||
value={data.name}
|
value={data.name}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(value) => updateArea(data.id, { name: value })}
|
onChange={(value) => updateArea(data.id, { name: value })}
|
||||||
onFocus={(e) => setEditField({ name: e.target.value })}
|
onFocus={(e) => setEditField({ name: e.target.value })}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
@@ -77,12 +79,14 @@ export default function AreaInfo({ data, i }) {
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
usePopover={true}
|
usePopover={true}
|
||||||
value={data.color}
|
value={data.color}
|
||||||
|
readOnly={layout.readOnly}
|
||||||
onChange={(color) => updateArea(i, { color })}
|
onChange={(color) => updateArea(i, { color })}
|
||||||
onColorPick={(color) => handleColorPick(color)}
|
onColorPick={(color) => handleColorPick(color)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDeleteStroked />}
|
|
||||||
type="danger"
|
type="danger"
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
onClick={() => deleteArea(i, true)}
|
onClick={() => deleteArea(i, true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Button } from "@douyinfe/semi-ui";
|
import { Button } from "@douyinfe/semi-ui";
|
||||||
import { IconPlus } from "@douyinfe/semi-icons";
|
import { IconPlus } from "@douyinfe/semi-icons";
|
||||||
import Empty from "../Empty";
|
import Empty from "../Empty";
|
||||||
import { useAreas } from "../../../hooks";
|
import { useAreas, useLayout } from "../../../hooks";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import AreaInfo from "./AreaDetails";
|
import AreaInfo from "./AreaDetails";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function AreasTab() {
|
export default function AreasTab() {
|
||||||
const { areas, addArea } = useAreas();
|
const { areas, addArea } = useAreas();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -15,7 +16,12 @@ export default function AreasTab() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<div>
|
<div>
|
||||||
<Button icon={<IconPlus />} block onClick={() => addArea()}>
|
<Button
|
||||||
|
icon={<IconPlus />}
|
||||||
|
block
|
||||||
|
onClick={() => addArea()}
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
>
|
||||||
{t("add_area")}
|
{t("add_area")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { ColorPicker as SemiColorPicker } from "@douyinfe/semi-ui";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function ColorPicker({
|
export default function ColorPicker({
|
||||||
children,
|
|
||||||
value,
|
value,
|
||||||
|
readOnly,
|
||||||
|
children,
|
||||||
onChange,
|
onChange,
|
||||||
onColorPick,
|
onColorPick,
|
||||||
...props
|
...props
|
||||||
@@ -25,6 +26,7 @@ export default function ColorPicker({
|
|||||||
{...props}
|
{...props}
|
||||||
value={SemiColorPicker.colorStringToValue(value)}
|
value={SemiColorPicker.colorStringToValue(value)}
|
||||||
onChange={({ hex: color }) => {
|
onChange={({ hex: color }) => {
|
||||||
|
if (readOnly) return;
|
||||||
setPickedColor(color);
|
setPickedColor(color);
|
||||||
onChange(color);
|
onChange(color);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button, Input, TagInput } from "@douyinfe/semi-ui";
|
import { Button, Input, TagInput } from "@douyinfe/semi-ui";
|
||||||
import { IconDeleteStroked } from "@douyinfe/semi-icons";
|
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 { Action, ObjectType } from "../../../data/constants";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function EnumDetails({ data, i }) {
|
export default function EnumDetails({ data, i }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { deleteEnum, updateEnum } = useEnums();
|
const { deleteEnum, updateEnum } = useEnums();
|
||||||
const { tables, updateField } = useDiagram();
|
const { tables, updateField } = useDiagram();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
@@ -18,6 +19,7 @@ export default function EnumDetails({ data, i }) {
|
|||||||
<div className="font-semibold">{t("Name")}: </div>
|
<div className="font-semibold">{t("Name")}: </div>
|
||||||
<Input
|
<Input
|
||||||
value={data.name}
|
value={data.name}
|
||||||
|
readonly={layout.readOnly}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
validateStatus={data.name.trim() === "" ? "error" : "default"}
|
validateStatus={data.name.trim() === "" ? "error" : "default"}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -71,7 +73,11 @@ export default function EnumDetails({ data, i }) {
|
|||||||
className="my-2"
|
className="my-2"
|
||||||
placeholder={t("values")}
|
placeholder={t("values")}
|
||||||
validateStatus={data.values.length === 0 ? "error" : "default"}
|
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 })}
|
onFocus={() => setEditField({ values: data.values })}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (JSON.stringify(editField.values) === JSON.stringify(data.values))
|
if (JSON.stringify(editField.values) === JSON.stringify(data.values))
|
||||||
@@ -95,8 +101,9 @@ export default function EnumDetails({ data, i }) {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
icon={<IconDeleteStroked />}
|
|
||||||
type="danger"
|
type="danger"
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
|
disabled={layout.readOnly}
|
||||||
onClick={() => deleteEnum(i, true)}
|
onClick={() => deleteEnum(i, true)}
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button, Collapse } from "@douyinfe/semi-ui";
|
import { Button, Collapse } from "@douyinfe/semi-ui";
|
||||||
import { useEnums } from "../../../hooks";
|
import { useEnums, useLayout } from "../../../hooks";
|
||||||
import { IconPlus } from "@douyinfe/semi-icons";
|
import { IconPlus } from "@douyinfe/semi-icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
@@ -8,6 +8,7 @@ import Empty from "../Empty";
|
|||||||
|
|
||||||
export default function EnumsTab() {
|
export default function EnumsTab() {
|
||||||
const { enums, addEnum } = useEnums();
|
const { enums, addEnum } = useEnums();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -15,7 +16,12 @@ export default function EnumsTab() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<div>
|
<div>
|
||||||
<Button icon={<IconPlus />} block onClick={() => addEnum()}>
|
<Button
|
||||||
|
block
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => addEnum()}
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
>
|
||||||
{t("add_enum")}
|
{t("add_enum")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { Button, Collapse, TextArea, Input } from "@douyinfe/semi-ui";
|
|||||||
import ColorPicker from "../ColorPicker";
|
import ColorPicker from "../ColorPicker";
|
||||||
import { IconDeleteStroked } from "@douyinfe/semi-icons";
|
import { IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||||
import { Action, ObjectType } from "../../../data/constants";
|
import { Action, ObjectType } from "../../../data/constants";
|
||||||
import { useNotes, useUndoRedo } from "../../../hooks";
|
import { useLayout, useNotes, useUndoRedo } from "../../../hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function NoteInfo({ data, nid }) {
|
export default function NoteInfo({ data, nid }) {
|
||||||
|
const { layout } = useLayout();
|
||||||
const { updateNote, deleteNote } = useNotes();
|
const { updateNote, deleteNote } = useNotes();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const [editField, setEditField] = useState({});
|
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>
|
<div className="font-semibold me-2 break-keep">{t("title")}:</div>
|
||||||
<Input
|
<Input
|
||||||
value={data.title}
|
value={data.title}
|
||||||
|
readonly={layout.readOnly}
|
||||||
placeholder={t("title")}
|
placeholder={t("title")}
|
||||||
onChange={(value) => updateNote(data.id, { title: value })}
|
onChange={(value) => updateNote(data.id, { title: value })}
|
||||||
onFocus={(e) => setEditField({ title: e.target.value })}
|
onFocus={(e) => setEditField({ title: e.target.value })}
|
||||||
@@ -90,6 +92,7 @@ export default function NoteInfo({ data, nid }) {
|
|||||||
placeholder={t("content")}
|
placeholder={t("content")}
|
||||||
value={data.content}
|
value={data.content}
|
||||||
autosize
|
autosize
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
const textarea = document.getElementById(`note_${data.id}`);
|
const textarea = document.getElementById(`note_${data.id}`);
|
||||||
textarea.style.height = "0";
|
textarea.style.height = "0";
|
||||||
@@ -127,13 +130,15 @@ export default function NoteInfo({ data, nid }) {
|
|||||||
<div className="ms-2 flex flex-col gap-2">
|
<div className="ms-2 flex flex-col gap-2">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
usePopover={true}
|
usePopover={true}
|
||||||
|
readOnly={layout.readOnly}
|
||||||
value={data.color}
|
value={data.color}
|
||||||
onChange={(color) => updateNote(data.id, { color })}
|
onChange={(color) => updateNote(data.id, { color })}
|
||||||
onColorPick={(color) => handleColorPick(color)}
|
onColorPick={(color) => handleColorPick(color)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDeleteStroked />}
|
|
||||||
type="danger"
|
type="danger"
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
onClick={() => deleteNote(nid, true)}
|
onClick={() => deleteNote(nid, true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button, Collapse } from "@douyinfe/semi-ui";
|
import { Button, Collapse } from "@douyinfe/semi-ui";
|
||||||
import { IconPlus } from "@douyinfe/semi-icons";
|
import { IconPlus } from "@douyinfe/semi-icons";
|
||||||
import { useNotes, useSelect } from "../../../hooks";
|
import { useLayout, useNotes, useSelect } from "../../../hooks";
|
||||||
import Empty from "../Empty";
|
import Empty from "../Empty";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import NoteInfo from "./NoteInfo";
|
import NoteInfo from "./NoteInfo";
|
||||||
@@ -10,6 +10,7 @@ export default function NotesTab() {
|
|||||||
const { notes, addNote } = useNotes();
|
const { notes, addNote } = useNotes();
|
||||||
const { selectedElement, setSelectedElement } = useSelect();
|
const { selectedElement, setSelectedElement } = useSelect();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { layout } = useLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -23,7 +24,12 @@ export default function NotesTab() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Button icon={<IconPlus />} block onClick={() => addNote()}>
|
<Button
|
||||||
|
block
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => addNote()}
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
>
|
||||||
{t("add_note")}
|
{t("add_note")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
Action,
|
Action,
|
||||||
ObjectType,
|
ObjectType,
|
||||||
} from "../../../data/constants";
|
} from "../../../data/constants";
|
||||||
import { useDiagram, useUndoRedo } from "../../../hooks";
|
import { useDiagram, useLayout, useUndoRedo } from "../../../hooks";
|
||||||
import i18n from "../../../i18n/i18n";
|
import i18n from "../../../i18n/i18n";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
@@ -38,6 +38,7 @@ export default function RelationshipInfo({ data }) {
|
|||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const { tables, deleteRelationship, updateRelationship } = useDiagram();
|
const { tables, deleteRelationship, updateRelationship } = useDiagram();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { layout } = useLayout();
|
||||||
const [editField, setEditField] = useState({});
|
const [editField, setEditField] = useState({});
|
||||||
|
|
||||||
const relValues = useMemo(() => {
|
const relValues = useMemo(() => {
|
||||||
@@ -98,6 +99,8 @@ export default function RelationshipInfo({ data }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changeCardinality = (value) => {
|
const changeCardinality = (value) => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
|
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -117,6 +120,8 @@ export default function RelationshipInfo({ data }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changeConstraint = (key, value) => {
|
const changeConstraint = (key, value) => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
|
|
||||||
const undoKey = `${key}Constraint`;
|
const undoKey = `${key}Constraint`;
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -145,6 +150,7 @@ export default function RelationshipInfo({ data }) {
|
|||||||
validateStatus={data.name.trim() === "" ? "error" : "default"}
|
validateStatus={data.name.trim() === "" ? "error" : "default"}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
className="ms-2"
|
className="ms-2"
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(value) => updateRelationship(data.id, { name: value })}
|
onChange={(value) => updateRelationship(data.id, { name: value })}
|
||||||
onFocus={(e) => setEditField({ name: e.target.value })}
|
onFocus={(e) => setEditField({ name: e.target.value })}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
@@ -196,9 +202,10 @@ export default function RelationshipInfo({ data }) {
|
|||||||
/>
|
/>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Button
|
<Button
|
||||||
icon={<IconLoopTextStroked />}
|
|
||||||
block
|
block
|
||||||
|
icon={<IconLoopTextStroked />}
|
||||||
onClick={swapKeys}
|
onClick={swapKeys}
|
||||||
|
disabled={layout.readOnly}
|
||||||
>
|
>
|
||||||
{t("swap")}
|
{t("swap")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -234,7 +241,7 @@ export default function RelationshipInfo({ data }) {
|
|||||||
placeholder={t("label")}
|
placeholder={t("label")}
|
||||||
onChange={(value) => updateRelationship(data.id, { manyLabel: value })}
|
onChange={(value) => updateRelationship(data.id, { manyLabel: value })}
|
||||||
onFocus={(e) => setEditField({ manyLabel: e.target.value })}
|
onFocus={(e) => setEditField({ manyLabel: e.target.value })}
|
||||||
defaultValue="n"
|
readonly={layout.readOnly}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
if (e.target.value === editField.manyLabel) return;
|
if (e.target.value === editField.manyLabel) return;
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
@@ -285,9 +292,10 @@ export default function RelationshipInfo({ data }) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDeleteStroked />}
|
|
||||||
block
|
block
|
||||||
type="danger"
|
type="danger"
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
onClick={() => deleteRelationship(data.id)}
|
onClick={() => deleteRelationship(data.id)}
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import {
|
|||||||
} from "@douyinfe/semi-ui";
|
} from "@douyinfe/semi-ui";
|
||||||
import { Action, ObjectType } from "../../../data/constants";
|
import { Action, ObjectType } from "../../../data/constants";
|
||||||
import { IconDeleteStroked } from "@douyinfe/semi-icons";
|
import { IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||||
import { useDiagram, useUndoRedo } from "../../../hooks";
|
import { useDiagram, useLayout, useUndoRedo } from "../../../hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { dbToTypes } from "../../../data/datatypes";
|
import { dbToTypes } from "../../../data/datatypes";
|
||||||
import { databases } from "../../../data/databases";
|
import { databases } from "../../../data/databases";
|
||||||
|
|
||||||
export default function FieldDetails({ data, tid }) {
|
export default function FieldDetails({ data, tid }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { tables, database } = useDiagram();
|
const { tables, database } = useDiagram();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const { updateField, deleteField } = useDiagram();
|
const { updateField, deleteField } = useDiagram();
|
||||||
@@ -29,6 +30,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
className="my-2"
|
className="my-2"
|
||||||
placeholder={t("default_value")}
|
placeholder={t("default_value")}
|
||||||
value={data.default}
|
value={data.default}
|
||||||
|
readonly={layout.readOnly}
|
||||||
disabled={dbToTypes[database][data.type].noDefault || data.increment}
|
disabled={dbToTypes[database][data.type].noDefault || data.increment}
|
||||||
onChange={(value) => updateField(tid, data.id, { default: value })}
|
onChange={(value) => updateField(tid, data.id, { default: value })}
|
||||||
onFocus={(e) => setEditField({ default: e.target.value })}
|
onFocus={(e) => setEditField({ default: e.target.value })}
|
||||||
@@ -67,7 +69,10 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
addOnBlur
|
addOnBlur
|
||||||
className="my-2"
|
className="my-2"
|
||||||
placeholder={t("use_for_batch_input")}
|
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 })}
|
onFocus={() => setEditField({ values: data.values })}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (
|
if (
|
||||||
@@ -102,6 +107,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
className="my-2 w-full"
|
className="my-2 w-full"
|
||||||
placeholder={t("size")}
|
placeholder={t("size")}
|
||||||
value={data.size}
|
value={data.size}
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(value) => updateField(tid, data.id, { size: value })}
|
onChange={(value) => updateField(tid, data.id, { size: value })}
|
||||||
onFocus={(e) => setEditField({ size: e.target.value })}
|
onFocus={(e) => setEditField({ size: e.target.value })}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
@@ -138,6 +144,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
? "default"
|
? "default"
|
||||||
: "error"
|
: "error"
|
||||||
}
|
}
|
||||||
|
readonly={layout.readOnly}
|
||||||
value={data.size}
|
value={data.size}
|
||||||
onChange={(value) => updateField(tid, data.id, { size: value })}
|
onChange={(value) => updateField(tid, data.id, { size: value })}
|
||||||
onFocus={(e) => setEditField({ size: e.target.value })}
|
onFocus={(e) => setEditField({ size: e.target.value })}
|
||||||
@@ -172,6 +179,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
placeholder={t("check")}
|
placeholder={t("check")}
|
||||||
value={data.check}
|
value={data.check}
|
||||||
disabled={data.increment}
|
disabled={data.increment}
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(value) => updateField(tid, data.id, { check: value })}
|
onChange={(value) => updateField(tid, data.id, { check: value })}
|
||||||
onFocus={(e) => setEditField({ check: e.target.value })}
|
onFocus={(e) => setEditField({ check: e.target.value })}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
@@ -203,6 +211,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
value="unique"
|
value="unique"
|
||||||
checked={data.unique}
|
checked={data.unique}
|
||||||
|
disabled={layout.readOnly}
|
||||||
onChange={(checkedValues) => {
|
onChange={(checkedValues) => {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -233,7 +242,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
value="increment"
|
value="increment"
|
||||||
checked={data.increment}
|
checked={data.increment}
|
||||||
disabled={
|
disabled={
|
||||||
!dbToTypes[database][data.type].canIncrement || data.isArray
|
!dbToTypes[database][data.type].canIncrement || data.isArray || layout.readOnly
|
||||||
}
|
}
|
||||||
onChange={(checkedValues) => {
|
onChange={(checkedValues) => {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
@@ -270,6 +279,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
value="isArray"
|
value="isArray"
|
||||||
checked={data.isArray}
|
checked={data.isArray}
|
||||||
|
disabled={layout.readOnly}
|
||||||
onChange={(checkedValues) => {
|
onChange={(checkedValues) => {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -307,6 +317,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
value="unsigned"
|
value="unsigned"
|
||||||
checked={data.unsigned}
|
checked={data.unsigned}
|
||||||
|
disabled={layout.readOnly}
|
||||||
onChange={(checkedValues) => {
|
onChange={(checkedValues) => {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -343,6 +354,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
className="my-2"
|
className="my-2"
|
||||||
placeholder={t("comment")}
|
placeholder={t("comment")}
|
||||||
value={data.comment}
|
value={data.comment}
|
||||||
|
readonly={layout.readOnly}
|
||||||
autosize
|
autosize
|
||||||
rows={2}
|
rows={2}
|
||||||
onChange={(value) => updateField(tid, data.id, { comment: value })}
|
onChange={(value) => updateField(tid, data.id, { comment: value })}
|
||||||
@@ -372,6 +384,7 @@ export default function FieldDetails({ data, tid }) {
|
|||||||
icon={<IconDeleteStroked />}
|
icon={<IconDeleteStroked />}
|
||||||
type="danger"
|
type="danger"
|
||||||
block
|
block
|
||||||
|
disabled={layout.readOnly}
|
||||||
onClick={() => deleteField(data, tid)}
|
onClick={() => deleteField(data, tid)}
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Action, ObjectType } from "../../../data/constants";
|
import { Action, ObjectType } from "../../../data/constants";
|
||||||
import { Input, Button, Popover, Checkbox, Select } from "@douyinfe/semi-ui";
|
import { Input, Button, Popover, Checkbox, Select } from "@douyinfe/semi-ui";
|
||||||
import { IconMore, IconDeleteStroked } from "@douyinfe/semi-icons";
|
import { IconMore, IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||||
import { useDiagram, useUndoRedo } from "../../../hooks";
|
import { useDiagram, useLayout, useUndoRedo } from "../../../hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
export default function IndexDetails({ data, fields, iid, tid }) {
|
export default function IndexDetails({ data, fields, iid, tid }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { tables, updateTable } = useDiagram();
|
const { tables, updateTable } = useDiagram();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const [editField, setEditField] = useState({});
|
const [editField, setEditField] = useState({});
|
||||||
@@ -22,6 +23,8 @@ export default function IndexDetails({ data, fields, iid, tid }) {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
value={data.fields}
|
value={data.fields}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
|
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -62,6 +65,7 @@ export default function IndexDetails({ data, fields, iid, tid }) {
|
|||||||
<Input
|
<Input
|
||||||
value={data.name}
|
value={data.name}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
|
readonly={layout.readOnly}
|
||||||
validateStatus={data.name.trim() === "" ? "error" : "default"}
|
validateStatus={data.name.trim() === "" ? "error" : "default"}
|
||||||
onFocus={() =>
|
onFocus={() =>
|
||||||
setEditField({
|
setEditField({
|
||||||
@@ -106,6 +110,7 @@ export default function IndexDetails({ data, fields, iid, tid }) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
value="unique"
|
value="unique"
|
||||||
checked={data.unique}
|
checked={data.unique}
|
||||||
|
disabled={layout.readOnly}
|
||||||
onChange={(checkedValues) => {
|
onChange={(checkedValues) => {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -145,9 +150,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
|
|||||||
></Checkbox>
|
></Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDeleteStroked />}
|
|
||||||
type="danger"
|
|
||||||
block
|
block
|
||||||
|
type="danger"
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { useMemo, useState } from "react";
|
|||||||
import { Action, ObjectType } from "../../../data/constants";
|
import { Action, ObjectType } from "../../../data/constants";
|
||||||
import { Input, Button, Popover, Select } from "@douyinfe/semi-ui";
|
import { Input, Button, Popover, Select } from "@douyinfe/semi-ui";
|
||||||
import { IconMore, IconKeyStroked } from "@douyinfe/semi-icons";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { dbToTypes } from "../../../data/datatypes";
|
import { dbToTypes } from "../../../data/datatypes";
|
||||||
import { DragHandle } from "../../SortableList/DragHandle";
|
import { DragHandle } from "../../SortableList/DragHandle";
|
||||||
@@ -12,6 +18,7 @@ export default function TableField({ data, tid, index, inherited }) {
|
|||||||
const { updateField } = useDiagram();
|
const { updateField } = useDiagram();
|
||||||
const { types } = useTypes();
|
const { types } = useTypes();
|
||||||
const { enums } = useEnums();
|
const { enums } = useEnums();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { tables, database } = useDiagram();
|
const { tables, database } = useDiagram();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
@@ -20,7 +27,7 @@ export default function TableField({ data, tid, index, inherited }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hover-1 my-2 flex gap-2 items-center">
|
<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">
|
<div className="min-w-20 flex-1/3">
|
||||||
<Input
|
<Input
|
||||||
@@ -29,6 +36,7 @@ export default function TableField({ data, tid, index, inherited }) {
|
|||||||
validateStatus={
|
validateStatus={
|
||||||
data.name.trim() === "" || inherited ? "error" : "default"
|
data.name.trim() === "" || inherited ? "error" : "default"
|
||||||
}
|
}
|
||||||
|
readonly={layout.readOnly}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
onChange={(value) => updateField(tid, data.id, { name: value })}
|
onChange={(value) => updateField(tid, data.id, { name: value })}
|
||||||
onFocus={(e) => setEditField({ name: e.target.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"}
|
validateStatus={data.type === "" ? "error" : "default"}
|
||||||
placeholder={t("type")}
|
placeholder={t("type")}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
|
|
||||||
if (value === data.type) return;
|
if (value === data.type) return;
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -142,10 +152,12 @@ export default function TableField({ data, tid, index, inherited }) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type={data.notNull ? "tertiary" : "primary"}
|
|
||||||
title={t("nullable")}
|
title={t("nullable")}
|
||||||
|
type={data.notNull ? "tertiary" : "primary"}
|
||||||
theme={data.notNull ? "light" : "solid"}
|
theme={data.notNull ? "light" : "solid"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
|
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -172,11 +184,13 @@ export default function TableField({ data, tid, index, inherited }) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type={data.primary ? "primary" : "tertiary"}
|
|
||||||
title={t("primary")}
|
title={t("primary")}
|
||||||
theme={data.primary ? "solid" : "light"}
|
theme={data.primary ? "solid" : "light"}
|
||||||
|
type={data.primary ? "primary" : "tertiary"}
|
||||||
icon={<IconKeyStroked />}
|
icon={<IconKeyStroked />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
|
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import {
|
|||||||
} from "@douyinfe/semi-ui";
|
} from "@douyinfe/semi-ui";
|
||||||
import ColorPicker from "../ColorPicker";
|
import ColorPicker from "../ColorPicker";
|
||||||
import { IconDeleteStroked } from "@douyinfe/semi-icons";
|
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 { Action, ObjectType, State, DB } from "../../../data/constants";
|
||||||
import TableField from "./TableField";
|
import TableField from "./TableField";
|
||||||
import IndexDetails from "./IndexDetails";
|
import IndexDetails from "./IndexDetails";
|
||||||
@@ -21,6 +26,7 @@ export default function TableInfo({ data }) {
|
|||||||
const { tables, database } = useDiagram();
|
const { tables, database } = useDiagram();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [indexActiveKey, setIndexActiveKey] = useState("");
|
const [indexActiveKey, setIndexActiveKey] = useState("");
|
||||||
|
const { layout } = useLayout();
|
||||||
const { deleteTable, updateTable, setTables } = useDiagram();
|
const { deleteTable, updateTable, setTables } = useDiagram();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const { setSaveState } = useSaveState();
|
const { setSaveState } = useSaveState();
|
||||||
@@ -82,6 +88,7 @@ export default function TableInfo({ data }) {
|
|||||||
validateStatus={data.name.trim() === "" ? "error" : "default"}
|
validateStatus={data.name.trim() === "" ? "error" : "default"}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
className="ms-2"
|
className="ms-2"
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(value) => updateTable(data.id, { name: value })}
|
onChange={(value) => updateTable(data.id, { name: value })}
|
||||||
onFocus={(e) => setEditField({ name: e.target.value })}
|
onFocus={(e) => setEditField({ name: e.target.value })}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
@@ -139,6 +146,8 @@ export default function TableInfo({ data }) {
|
|||||||
.filter((t) => t.id !== data.id)
|
.filter((t) => t.id !== data.id)
|
||||||
.map((t) => ({ label: t.name, value: t.name }))}
|
.map((t) => ({ label: t.name, value: t.name }))}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
|
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -204,6 +213,7 @@ export default function TableInfo({ data }) {
|
|||||||
<TextArea
|
<TextArea
|
||||||
field="comment"
|
field="comment"
|
||||||
value={data.comment}
|
value={data.comment}
|
||||||
|
readonly={layout.readOnly}
|
||||||
autosize
|
autosize
|
||||||
placeholder={t("comment")}
|
placeholder={t("comment")}
|
||||||
rows={1}
|
rows={1}
|
||||||
@@ -238,6 +248,7 @@ export default function TableInfo({ data }) {
|
|||||||
<div className="flex justify-between items-center gap-1 mb-2">
|
<div className="flex justify-between items-center gap-1 mb-2">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
usePopover={true}
|
usePopover={true}
|
||||||
|
readOnly={layout.readOnly}
|
||||||
value={data.color}
|
value={data.color}
|
||||||
onChange={(color) => updateTable(data.id, { color })}
|
onChange={(color) => updateTable(data.id, { color })}
|
||||||
onColorPick={(color) => handleColorPick(color)}
|
onColorPick={(color) => handleColorPick(color)}
|
||||||
@@ -245,6 +256,7 @@ export default function TableInfo({ data }) {
|
|||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
|
disabled={layout.readOnly}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIndexActiveKey("1");
|
setIndexActiveKey("1");
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
@@ -277,6 +289,8 @@ export default function TableInfo({ data }) {
|
|||||||
{t("add_index")}
|
{t("add_index")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
block
|
||||||
|
disabled={layout.readOnly}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
@@ -312,13 +326,13 @@ export default function TableInfo({ data }) {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
block
|
|
||||||
>
|
>
|
||||||
{t("add_field")}
|
{t("add_field")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDeleteStroked />}
|
|
||||||
type="danger"
|
type="danger"
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
onClick={() => deleteTable(data.id)}
|
onClick={() => deleteTable(data.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Collapse, Button } from "@douyinfe/semi-ui";
|
import { Collapse, Button } from "@douyinfe/semi-ui";
|
||||||
import { IconPlus } from "@douyinfe/semi-icons";
|
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 { ObjectType, State } from "../../../data/constants";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DragHandle } from "../../SortableList/DragHandle";
|
import { DragHandle } from "../../SortableList/DragHandle";
|
||||||
@@ -13,6 +13,7 @@ export default function TablesTab() {
|
|||||||
const { tables, addTable, setTables } = useDiagram();
|
const { tables, addTable, setTables } = useDiagram();
|
||||||
const { selectedElement, setSelectedElement } = useSelect();
|
const { selectedElement, setSelectedElement } = useSelect();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { setSaveState } = useSaveState();
|
const { setSaveState } = useSaveState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,7 +21,12 @@ export default function TablesTab() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<SearchBar tables={tables} />
|
<SearchBar tables={tables} />
|
||||||
<div>
|
<div>
|
||||||
<Button icon={<IconPlus />} block onClick={() => addTable()}>
|
<Button
|
||||||
|
block
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => addTable()}
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
>
|
||||||
{t("add_table")}
|
{t("add_table")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,6 +66,8 @@ export default function TablesTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TableListItem({ table }) {
|
function TableListItem({ table }) {
|
||||||
|
const { layout } = useLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={`scroll_table_${table.id}`}>
|
<div id={`scroll_table_${table.id}`}>
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
@@ -67,7 +75,7 @@ function TableListItem({ table }) {
|
|||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{table.name}
|
{table.name}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,13 +11,20 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
} from "@douyinfe/semi-ui";
|
} from "@douyinfe/semi-ui";
|
||||||
import { IconDeleteStroked, IconMore } from "@douyinfe/semi-icons";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { dbToTypes } from "../../../data/datatypes";
|
import { dbToTypes } from "../../../data/datatypes";
|
||||||
|
|
||||||
export default function TypeField({ data, tid, fid }) {
|
export default function TypeField({ data, tid, fid }) {
|
||||||
const { types, updateType } = useTypes();
|
const { types, updateType } = useTypes();
|
||||||
const { enums } = useEnums();
|
const { enums } = useEnums();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { database } = useDiagram();
|
const { database } = useDiagram();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const [editField, setEditField] = useState({});
|
const [editField, setEditField] = useState({});
|
||||||
@@ -28,6 +35,7 @@ export default function TypeField({ data, tid, fid }) {
|
|||||||
<Col span={10}>
|
<Col span={10}>
|
||||||
<Input
|
<Input
|
||||||
value={data.name}
|
value={data.name}
|
||||||
|
readonly={layout.readOnly}
|
||||||
validateStatus={data.name === "" ? "error" : "default"}
|
validateStatus={data.name === "" ? "error" : "default"}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
@@ -86,6 +94,7 @@ export default function TypeField({ data, tid, fid }) {
|
|||||||
validateStatus={data.type === "" ? "error" : "default"}
|
validateStatus={data.type === "" ? "error" : "default"}
|
||||||
placeholder={t("type")}
|
placeholder={t("type")}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
if (value === data.type) return;
|
if (value === data.type) return;
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -160,13 +169,14 @@ export default function TypeField({ data, tid, fid }) {
|
|||||||
}
|
}
|
||||||
className="my-2"
|
className="my-2"
|
||||||
placeholder={t("use_for_batch_input")}
|
placeholder={t("use_for_batch_input")}
|
||||||
onChange={(v) =>
|
onChange={(v) => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
updateType(tid, {
|
updateType(tid, {
|
||||||
fields: types[tid].fields.map((e, id) =>
|
fields: types[tid].fields.map((e, id) =>
|
||||||
id === fid ? { ...data, values: v } : e,
|
id === fid ? { ...data, values: v } : e,
|
||||||
),
|
),
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
onFocus={() => setEditField({ values: data.values })}
|
onFocus={() => setEditField({ values: data.values })}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (
|
if (
|
||||||
@@ -202,6 +212,7 @@ export default function TypeField({ data, tid, fid }) {
|
|||||||
className="my-2 w-full"
|
className="my-2 w-full"
|
||||||
placeholder={t("size")}
|
placeholder={t("size")}
|
||||||
value={data.size}
|
value={data.size}
|
||||||
|
readonly={layout.readOnly}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
updateType(tid, {
|
updateType(tid, {
|
||||||
fields: types[tid].fields.map((e, id) =>
|
fields: types[tid].fields.map((e, id) =>
|
||||||
@@ -239,6 +250,7 @@ export default function TypeField({ data, tid, fid }) {
|
|||||||
<Input
|
<Input
|
||||||
className="my-2 w-full"
|
className="my-2 w-full"
|
||||||
placeholder={t("set_precision")}
|
placeholder={t("set_precision")}
|
||||||
|
readonly={layout.readOnly}
|
||||||
validateStatus={
|
validateStatus={
|
||||||
/^\(\d+,\s*\d+\)$|^$/.test(data.size)
|
/^\(\d+,\s*\d+\)$|^$/.test(data.size)
|
||||||
? "default"
|
? "default"
|
||||||
@@ -277,9 +289,10 @@ export default function TypeField({ data, tid, fid }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDeleteStroked />}
|
|
||||||
block
|
block
|
||||||
type="danger"
|
type="danger"
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
} from "@douyinfe/semi-ui";
|
} from "@douyinfe/semi-ui";
|
||||||
import { IconDeleteStroked, IconPlus } from "@douyinfe/semi-icons";
|
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 TypeField from "./TypeField";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function TypeInfo({ index, data }) {
|
export default function TypeInfo({ index, data }) {
|
||||||
|
const { layout } = useLayout();
|
||||||
const { deleteType, updateType } = useTypes();
|
const { deleteType, updateType } = useTypes();
|
||||||
const { tables, updateField } = useDiagram();
|
const { tables, updateField } = useDiagram();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
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>
|
<div className="text-md font-semibold break-keep">{t("name")}: </div>
|
||||||
<Input
|
<Input
|
||||||
value={data.name}
|
value={data.name}
|
||||||
|
readonly={layout.readOnly}
|
||||||
validateStatus={data.name === "" ? "error" : "default"}
|
validateStatus={data.name === "" ? "error" : "default"}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
className="ms-2"
|
className="ms-2"
|
||||||
@@ -97,6 +99,7 @@ export default function TypeInfo({ index, data }) {
|
|||||||
field="comment"
|
field="comment"
|
||||||
value={data.comment}
|
value={data.comment}
|
||||||
autosize
|
autosize
|
||||||
|
readonly={layout.readOnly}
|
||||||
placeholder={t("comment")}
|
placeholder={t("comment")}
|
||||||
rows={1}
|
rows={1}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
@@ -130,6 +133,7 @@ export default function TypeInfo({ index, data }) {
|
|||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconPlus />}
|
icon={<IconPlus />}
|
||||||
|
disabled={layout.readOnly}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -162,10 +166,11 @@ export default function TypeInfo({ index, data }) {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDeleteStroked />}
|
|
||||||
type="danger"
|
|
||||||
onClick={() => deleteType(index)}
|
|
||||||
block
|
block
|
||||||
|
type="danger"
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
|
onClick={() => deleteType(index)}
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Collapse, Button, Popover } from "@douyinfe/semi-ui";
|
import { Collapse, Button, Popover } from "@douyinfe/semi-ui";
|
||||||
import { IconPlus, IconInfoCircle } from "@douyinfe/semi-icons";
|
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 { DB, ObjectType } from "../../../data/constants";
|
||||||
import Searchbar from "./SearchBar";
|
import Searchbar from "./SearchBar";
|
||||||
import Empty from "../Empty";
|
import Empty from "../Empty";
|
||||||
@@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
export default function TypesTab() {
|
export default function TypesTab() {
|
||||||
const { types, addType } = useTypes();
|
const { types, addType } = useTypes();
|
||||||
const { selectedElement, setSelectedElement } = useSelect();
|
const { selectedElement, setSelectedElement } = useSelect();
|
||||||
|
const { layout } = useLayout();
|
||||||
const { database } = useDiagram();
|
const { database } = useDiagram();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -18,7 +19,12 @@ export default function TypesTab() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Searchbar />
|
<Searchbar />
|
||||||
<div>
|
<div>
|
||||||
<Button icon={<IconPlus />} block onClick={() => addType()}>
|
<Button
|
||||||
|
block
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => addType()}
|
||||||
|
disabled={layout.readOnly}
|
||||||
|
>
|
||||||
{t("add_type")}
|
{t("add_type")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { IconHandle } from "@douyinfe/semi-icons";
|
import { IconHandle } from "@douyinfe/semi-icons";
|
||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
|
|
||||||
export function DragHandle({ id }) {
|
export function DragHandle({ id, readOnly }) {
|
||||||
const { listeners } = useSortable({ id });
|
const { listeners } = useSortable({ id });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex cursor-move items-center justify-center opacity-50 mt-0.5"
|
className={`opacity-50 mt-0.5 ${readOnly ? "cursor-not-allowed" : "cursor-move"}`}
|
||||||
{...listeners}
|
{...(!readOnly && listeners)}
|
||||||
>
|
>
|
||||||
<IconHandle />
|
<IconHandle />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,28 +19,36 @@ import {
|
|||||||
useEnums,
|
useEnums,
|
||||||
} from "../hooks";
|
} from "../hooks";
|
||||||
import FloatingControls from "./FloatingControls";
|
import FloatingControls from "./FloatingControls";
|
||||||
import { Modal, Tag } from "@douyinfe/semi-ui";
|
import { Button, Modal, Tag } from "@douyinfe/semi-ui";
|
||||||
|
import { IconAlertTriangle } from "@douyinfe/semi-icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { databases } from "../data/databases";
|
import { databases } from "../data/databases";
|
||||||
import { isRtl } from "../i18n/utils/rtl";
|
import { isRtl } from "../i18n/utils/rtl";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { get } from "../api/gists";
|
import { get, SHARE_FILENAME } from "../api/gists";
|
||||||
|
|
||||||
export const IdContext = createContext({ gistId: "", setGistId: () => {} });
|
export const IdContext = createContext({
|
||||||
|
gistId: "",
|
||||||
|
setGistId: () => {},
|
||||||
|
version: "",
|
||||||
|
setVersion: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
const SIDEPANEL_MIN_WIDTH = 384;
|
const SIDEPANEL_MIN_WIDTH = 384;
|
||||||
|
|
||||||
export default function WorkSpace() {
|
export default function WorkSpace() {
|
||||||
const [id, setId] = useState(0);
|
const [id, setId] = useState(0);
|
||||||
const [gistId, setGistId] = useState("");
|
const [gistId, setGistId] = useState("");
|
||||||
|
const [version, setVersion] = useState("");
|
||||||
const [loadedFromGistId, setLoadedFromGistId] = useState("");
|
const [loadedFromGistId, setLoadedFromGistId] = useState("");
|
||||||
const [title, setTitle] = useState("Untitled Diagram");
|
const [title, setTitle] = useState("Untitled Diagram");
|
||||||
const [resize, setResize] = useState(false);
|
const [resize, setResize] = useState(false);
|
||||||
const [width, setWidth] = useState(SIDEPANEL_MIN_WIDTH);
|
const [width, setWidth] = useState(SIDEPANEL_MIN_WIDTH);
|
||||||
const [lastSaved, setLastSaved] = useState("");
|
const [lastSaved, setLastSaved] = useState("");
|
||||||
const [showSelectDbModal, setShowSelectDbModal] = useState(false);
|
const [showSelectDbModal, setShowSelectDbModal] = useState(false);
|
||||||
|
const [showRestoreModal, setShowRestoreModal] = useState(false);
|
||||||
const [selectedDb, setSelectedDb] = useState("");
|
const [selectedDb, setSelectedDb] = useState("");
|
||||||
const { layout } = useLayout();
|
const { layout, setLayout } = useLayout();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { types, setTypes } = useTypes();
|
const { types, setTypes } = useTypes();
|
||||||
const { areas, setAreas } = useAreas();
|
const { areas, setAreas } = useAreas();
|
||||||
@@ -67,8 +75,6 @@ export default function WorkSpace() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = useCallback(async () => {
|
||||||
if (saveState !== State.SAVING) return;
|
|
||||||
|
|
||||||
const name = window.name.split(" ");
|
const name = window.name.split(" ");
|
||||||
const op = name[0];
|
const op = name[0];
|
||||||
const saveAsDiagram = window.name === "" || op === "d" || op === "lt";
|
const saveAsDiagram = window.name === "" || op === "d" || op === "lt";
|
||||||
@@ -163,7 +169,6 @@ export default function WorkSpace() {
|
|||||||
enums,
|
enums,
|
||||||
gistId,
|
gistId,
|
||||||
loadedFromGistId,
|
loadedFromGistId,
|
||||||
saveState,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
@@ -287,25 +292,24 @@ export default function WorkSpace() {
|
|||||||
|
|
||||||
const loadFromGist = async (shareId) => {
|
const loadFromGist = async (shareId) => {
|
||||||
try {
|
try {
|
||||||
const res = await get(shareId);
|
const { data } = await get(shareId);
|
||||||
const diagramSrc = res.data.files["share.json"].content;
|
const parsedDiagram = JSON.parse(data.files[SHARE_FILENAME].content);
|
||||||
const d = JSON.parse(diagramSrc);
|
|
||||||
setGistId(shareId);
|
|
||||||
setUndoStack([]);
|
setUndoStack([]);
|
||||||
setRedoStack([]);
|
setRedoStack([]);
|
||||||
|
setGistId(shareId);
|
||||||
setLoadedFromGistId(shareId);
|
setLoadedFromGistId(shareId);
|
||||||
setDatabase(d.database);
|
setDatabase(parsedDiagram.database);
|
||||||
setTitle(d.title);
|
setTitle(parsedDiagram.title);
|
||||||
setTables(d.tables);
|
setTables(parsedDiagram.tables);
|
||||||
setRelationships(d.relationships);
|
setRelationships(parsedDiagram.relationships);
|
||||||
setNotes(d.notes);
|
setNotes(parsedDiagram.notes);
|
||||||
setAreas(d.subjectAreas);
|
setAreas(parsedDiagram.subjectAreas);
|
||||||
setTransform(d.transform);
|
setTransform(parsedDiagram.transform);
|
||||||
if (databases[d.database].hasTypes) {
|
if (databases[parsedDiagram.database].hasTypes) {
|
||||||
setTypes(d.types ?? []);
|
setTypes(parsedDiagram.types ?? []);
|
||||||
}
|
}
|
||||||
if (databases[d.database].hasEnums) {
|
if (databases[parsedDiagram.database].hasEnums) {
|
||||||
setEnums(d.enums ?? []);
|
setEnums(parsedDiagram.enums ?? []);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@@ -368,6 +372,12 @@ export default function WorkSpace() {
|
|||||||
searchParams,
|
searchParams,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const returnToCurrentDiagram = async () => {
|
||||||
|
await load();
|
||||||
|
setLayout((prev) => ({ ...prev, readOnly: false }));
|
||||||
|
setVersion(null);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
tables?.length === 0 &&
|
tables?.length === 0 &&
|
||||||
@@ -398,8 +408,12 @@ export default function WorkSpace() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (layout.readOnly) return;
|
||||||
|
|
||||||
|
if (saveState !== State.SAVING) return;
|
||||||
|
|
||||||
save();
|
save();
|
||||||
}, [saveState, save]);
|
}, [saveState, layout, save]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = "Editor | drawDB";
|
document.title = "Editor | drawDB";
|
||||||
@@ -409,7 +423,7 @@ export default function WorkSpace() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col overflow-hidden theme">
|
<div className="h-full flex flex-col overflow-hidden theme">
|
||||||
<IdContext.Provider value={{ gistId, setGistId }}>
|
<IdContext.Provider value={{ gistId, setGistId, version, setVersion }}>
|
||||||
<ControlPanel
|
<ControlPanel
|
||||||
diagramId={id}
|
diagramId={id}
|
||||||
setDiagramId={setId}
|
setDiagramId={setId}
|
||||||
@@ -438,6 +452,23 @@ export default function WorkSpace() {
|
|||||||
<CanvasContextProvider className="h-full w-full">
|
<CanvasContextProvider className="h-full w-full">
|
||||||
<Canvas saveState={saveState} setSaveState={setSaveState} />
|
<Canvas saveState={saveState} setSaveState={setSaveState} />
|
||||||
</CanvasContextProvider>
|
</CanvasContextProvider>
|
||||||
|
{version && (
|
||||||
|
<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) && (
|
{!(layout.sidebar || layout.toolbar || layout.header) && (
|
||||||
<div className="fixed right-5 bottom-4">
|
<div className="fixed right-5 bottom-4">
|
||||||
<FloatingControls />
|
<FloatingControls />
|
||||||
@@ -494,6 +525,27 @@ export default function WorkSpace() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
visible={showRestoreModal}
|
||||||
|
centered
|
||||||
|
closable
|
||||||
|
onCancel={() => setShowRestoreModal(false)}
|
||||||
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export default function LayoutContextProvider({ children }) {
|
|||||||
issues: true,
|
issues: true,
|
||||||
toolbar: true,
|
toolbar: true,
|
||||||
dbmlEditor: false,
|
dbmlEditor: false,
|
||||||
|
readOnly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export const SIDESHEET = {
|
|||||||
NONE: 0,
|
NONE: 0,
|
||||||
TODO: 1,
|
TODO: 1,
|
||||||
TIMELINE: 2,
|
TIMELINE: 2,
|
||||||
|
VERSIONS: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DB = {
|
export const DB = {
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ import { pl, polish } from "./locales/pl";
|
|||||||
import { no, norwegian } from "./locales/no";
|
import { no, norwegian } from "./locales/no";
|
||||||
import { sv, swedish } from "./locales/sv-se";
|
import { sv, swedish } from "./locales/sv-se";
|
||||||
import { ur, urdu } from "./locales/ur";
|
import { ur, urdu } from "./locales/ur";
|
||||||
import { jp, japanese} from "./locales/jp"
|
import { jp, japanese } from "./locales/jp";
|
||||||
import {ne, nepali} from "./locales/ne"
|
import { ne, nepali } from "./locales/ne";
|
||||||
import { ug, uyghur } from "./locales/ug";
|
import { ug, uyghur } from "./locales/ug";
|
||||||
import { pa_pk, punjabipk } from "./locales/pa-pk";
|
import { pa_pk, punjabipk } from "./locales/pa-pk";
|
||||||
import { cz, czech } from "./locales/cz";
|
import { cz, czech } from "./locales/cz";
|
||||||
@@ -80,7 +80,7 @@ export const languages = [
|
|||||||
nepali,
|
nepali,
|
||||||
uyghur,
|
uyghur,
|
||||||
punjabipk,
|
punjabipk,
|
||||||
czech
|
czech,
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
@@ -131,7 +131,7 @@ i18n
|
|||||||
ne,
|
ne,
|
||||||
ug,
|
ug,
|
||||||
"pa-PK": pa_pk,
|
"pa-PK": pa_pk,
|
||||||
cz
|
cz,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const en = {
|
|||||||
report_bug: "Report a bug",
|
report_bug: "Report a bug",
|
||||||
import: "Import",
|
import: "Import",
|
||||||
inherits: "Inherits",
|
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",
|
import_from: "Import from",
|
||||||
file: "File",
|
file: "File",
|
||||||
new: "New",
|
new: "New",
|
||||||
@@ -258,6 +259,23 @@ const en = {
|
|||||||
tab_view: "Tab view",
|
tab_view: "Tab view",
|
||||||
label: "Label",
|
label: "Label",
|
||||||
many_side_label: "Many(n) side label",
|
many_side_label: "Many(n) side label",
|
||||||
|
version: "Version",
|
||||||
|
versions: "Versions",
|
||||||
|
no_saved_versions: "No saved versions",
|
||||||
|
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 any changes.",
|
||||||
|
return_to_current: "Return to diagram",
|
||||||
|
no_changes_to_record: "No changes to record",
|
||||||
|
click_to_view: "Click to view",
|
||||||
|
load_more: "Load more",
|
||||||
|
clear_cache: "Clear cache",
|
||||||
|
cache_cleared: "Cache cleared",
|
||||||
|
failed_to_record_version: "Failed to record version",
|
||||||
|
failed_to_load_diagram: "Failed to load diagram",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
background-color: rgba(var(--semi-blue-6), 1);
|
background-color: rgba(var(--semi-blue-6), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.semi-steps-item-content,
|
||||||
|
.semi-steps-item-title {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
.semi-spin-wrapper {
|
.semi-spin-wrapper {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user