Merge pull request #110 from drawdb-io/i18n

Configure i18n and add simplified Chinese (#99)
This commit is contained in:
lilit
2024-05-17 04:00:43 +03:00
committed by GitHub
54 changed files with 1835 additions and 594 deletions

View File

@@ -1,18 +1,20 @@
import { Button } from "@douyinfe/semi-ui";
import { IconCheckboxTick } from "@douyinfe/semi-icons";
import { tableThemes } from "../data/constants";
import { useTranslation } from "react-i18next";
export default function ColorPalette({
currentColor,
onClearColor,
onPickColor,
}) {
const { t } = useTranslation();
return (
<div>
<div className="flex justify-between items-center p-2">
<div className="font-medium">Theme</div>
<div className="font-medium">{t("theme")}</div>
<Button type="tertiary" size="small" onClick={onClearColor}>
Clear
{t("clear")}
</Button>
</div>
<hr />

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { Button, Popover, Input, Toast } from "@douyinfe/semi-ui";
import { Button, Popover, Input } from "@douyinfe/semi-ui";
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
import {
Tab,
@@ -17,7 +17,8 @@ import {
useSaveState,
useTransform,
} from "../../hooks";
import ColorPalette from "../ColorPalette";
import ColorPalette from "../ColorPicker";
import { useTranslation } from "react-i18next";
export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
const [hovered, setHovered] = useState(false);
@@ -191,14 +192,15 @@ function EditPopoverContent({ data }) {
const { setSaveState } = useSaveState();
const { updateArea, deleteArea } = useAreas();
const { setUndoStack, setRedoStack } = useUndoRedo();
const { t } = useTranslation();
return (
<div className="popover-theme">
<div className="font-semibold mb-2 ms-1">Edit subject area</div>
<div className="font-semibold mb-2 ms-1">{t("edit")}</div>
<div className="w-[280px] flex items-center mb-2">
<Input
value={data.name}
placeholder="Name"
placeholder={t("name")}
className="me-2"
onChange={(value) => updateArea(data.id, { name: value })}
onFocus={(e) => setEditField({ name: e.target.value })}
@@ -212,7 +214,10 @@ function EditPopoverContent({ data }) {
aid: data.id,
undo: editField,
redo: { name: e.target.value },
message: `Edit area name to ${e.target.value}`,
message: t("edit_area", {
areaName: e.target.value,
extra: "[name]",
}),
},
]);
setRedoStack([]);
@@ -232,7 +237,10 @@ function EditPopoverContent({ data }) {
aid: data.id,
undo: { color: data.color },
redo: { color: c },
message: `Edit area color to ${c}`,
message: t("edit_area", {
areaName: data.name,
extra: "[color]",
}),
},
]);
setRedoStack([]);
@@ -263,12 +271,9 @@ function EditPopoverContent({ data }) {
icon={<IconDeleteStroked />}
type="danger"
block
onClick={() => {
Toast.success(`Area deleted!`);
deleteArea(data.id);
}}
onClick={() => deleteArea(data.id, true)}
>
Delete
{t("delete")}
</Button>
</div>
</div>

View File

@@ -20,8 +20,11 @@ import {
useNotes,
useLayout,
} from "../../hooks";
import { useTranslation } from "react-i18next";
import { diagram } from "../../data/heroDiagram";
export default function Canvas() {
const { t } = useTranslation();
const { tables, updateTable, relationships, addRelationship } = useTables();
const { areas, updateArea } = useAreas();
const { notes, updateNote } = useNotes();
@@ -278,7 +281,10 @@ export default function Canvas() {
toX: info.x,
toY: info.y,
id: dragging.id,
message: `Move ${info.name} to (${info.x}, ${info.y})`,
message: t("move_element", {
coords: `(${info.x}, ${info.y})`,
name: info.name,
}),
},
]);
setRedoStack([]);
@@ -291,7 +297,10 @@ export default function Canvas() {
action: Action.PAN,
undo: { x: panning.x, y: panning.y },
redo: transform.pan,
message: `Move diagram to (${transform.pan?.x}, ${transform.pan?.y})`,
message: t("move_element", {
coords: `(${transform?.pan.x}, ${transform?.pan.y})`,
name: diagram,
}),
},
]);
setRedoStack([]);
@@ -321,7 +330,10 @@ export default function Canvas() {
height: initCoords.height,
},
redo: areas[areaResize.id],
message: `Resize area`,
message: t("edit_area", {
areaName: areas[areaResize.id].name,
extra: "[resize]",
}),
},
]);
setRedoStack([]);
@@ -350,7 +362,7 @@ export default function Canvas() {
tables[linkingLine.startTableId].fields[linkingLine.startFieldId].type !==
tables[hoveredTable.tableId].fields[hoveredTable.field].type
) {
Toast.info("Cannot connect");
Toast.info(t("connot_connect"));
return;
}
if (

View File

@@ -6,7 +6,7 @@ import {
State,
noteThemes,
} from "../../data/constants";
import { Input, Button, Popover, Toast } from "@douyinfe/semi-ui";
import { Input, Button, Popover } from "@douyinfe/semi-ui";
import {
IconEdit,
IconDeleteStroked,
@@ -19,6 +19,7 @@ import {
useNotes,
useSaveState,
} from "../../hooks";
import { useTranslation } from "react-i18next";
export default function Note({ data, onMouseDown }) {
const w = 180;
@@ -27,6 +28,7 @@ export default function Note({ data, onMouseDown }) {
const [editField, setEditField] = useState({});
const [hovered, setHovered] = useState(false);
const { layout } = useLayout();
const { t } = useTranslation();
const { setSaveState } = useSaveState();
const { updateNote, deleteNote } = useNotes();
const { setUndoStack, setRedoStack } = useUndoRedo();
@@ -54,7 +56,10 @@ export default function Note({ data, onMouseDown }) {
nid: data.id,
undo: editField,
redo: { content: e.target.value, height: newHeight },
message: `Edit note content to "${e.target.value}"`,
message: t("edit_note", {
noteTitle: e.target.value,
extra: "[content]",
}),
},
]);
setRedoStack([]);
@@ -168,11 +173,11 @@ export default function Note({ data, onMouseDown }) {
stopPropagation
content={
<div className="popover-theme">
<div className="font-semibold mb-2 ms-1">Edit note</div>
<div className="font-semibold mb-2 ms-1">{t("edit")}</div>
<div className="w-[280px] flex items-center mb-2">
<Input
value={data.title}
placeholder="Title"
placeholder={t("title")}
className="me-2"
onChange={(value) =>
updateNote(data.id, { title: value })
@@ -190,7 +195,10 @@ export default function Note({ data, onMouseDown }) {
nid: data.id,
undo: editField,
redo: { title: e.target.value },
message: `Edit note title to "${e.target.value}"`,
message: t("edit_note", {
noteTitle: e.target.value,
extra: "[title]",
}),
},
]);
setRedoStack([]);
@@ -199,7 +207,9 @@ export default function Note({ data, onMouseDown }) {
<Popover
content={
<div className="popover-theme">
<div className="font-medium mb-1">Theme</div>
<div className="font-medium mb-1">
{t("theme")}
</div>
<hr />
<div className="py-3">
{noteThemes.map((c) => (
@@ -216,7 +226,10 @@ export default function Note({ data, onMouseDown }) {
nid: data.id,
undo: { color: data.color },
redo: { color: c },
message: `Edit note color to ${c}`,
message: t("edit_note", {
noteTitle: data.title,
extra: "[color]",
}),
},
]);
setRedoStack([]);
@@ -249,12 +262,9 @@ export default function Note({ data, onMouseDown }) {
icon={<IconDeleteStroked />}
type="danger"
block
onClick={() => {
Toast.success(`Note deleted!`);
deleteNote(data.id);
}}
onClick={() => deleteNote(data.id, true)}
>
Delete
{t("delete")}
</Button>
</div>
</div>

View File

@@ -13,9 +13,10 @@ import {
IconDeleteStroked,
IconKeyStroked,
} from "@douyinfe/semi-icons";
import { Popover, Tag, Button, Toast, SideSheet } from "@douyinfe/semi-ui";
import { Popover, Tag, Button, SideSheet } from "@douyinfe/semi-ui";
import { useLayout, useSettings, useTables, useSelect } from "../../hooks";
import TableInfo from "../EditorSidePanel/TablesTab/TableInfo";
import { useTranslation } from "react-i18next";
export default function Table(props) {
const [hoveredField, setHoveredField] = useState(-1);
@@ -29,6 +30,7 @@ export default function Table(props) {
const { layout } = useLayout();
const { deleteTable, deleteField } = useTables();
const { settings } = useSettings();
const { t } = useTranslation();
const { selectedElement, setSelectedElement } = useSelect();
const height =
@@ -110,9 +112,9 @@ export default function Table(props) {
content={
<div className="popover-theme">
<div className="mb-2">
<strong>Comment :</strong>{" "}
<strong>{t("comment")}:</strong>{" "}
{tableData.comment === "" ? (
"No comment"
t("not_set")
) : (
<div>{tableData.comment}</div>
)}
@@ -123,10 +125,10 @@ export default function Table(props) {
tableData.indices.length === 0 ? "" : "block"
}`}
>
Indices :
{t("indices")}:
</strong>{" "}
{tableData.indices.length === 0 ? (
"No indices"
t("not_set")
) : (
<div>
{tableData.indices.map((index, k) => (
@@ -156,12 +158,9 @@ export default function Table(props) {
type="danger"
block
style={{ marginTop: "8px" }}
onClick={() => {
Toast.success(`Table deleted!`);
deleteTable(tableData.id);
}}
onClick={() => deleteTable(tableData.id)}
>
Delete table
{t("delete")}
</Button>
</div>
}
@@ -196,37 +195,31 @@ export default function Table(props) {
<hr />
{e.primary && (
<Tag color="blue" className="me-2 my-2">
Primary
{t("primary")}
</Tag>
)}
{e.unique && (
<Tag color="amber" className="me-2 my-2">
Unique
{t("unique")}
</Tag>
)}
{e.notNull && (
<Tag color="purple" className="me-2 my-2">
Not null
{t("not_null")}
</Tag>
)}
{e.increment && (
<Tag color="green" className="me-2 my-2">
Increment
{t("autoincrement")}
</Tag>
)}
<p>
<strong>Default: </strong>
{e.default === "" ? "Not set" : e.default}
<strong>{t("default_value")}: </strong>
{e.default === "" ? t("not_set") : e.default}
</p>
<p>
<strong>Comment: </strong>
{e.comment === "" ? (
"No comment"
) : (
<div className="max-w-[260px] break-words">
{e.comment}
</div>
)}
<strong>{t("comment")}: </strong>
{e.comment === "" ? t("not_set") : e.comment}
</p>
</div>
}
@@ -242,7 +235,7 @@ export default function Table(props) {
</div>
</foreignObject>
<SideSheet
title="Edit table"
title={t("edit")}
size="small"
visible={
selectedElement.element === ObjectType.TABLE &&

View File

@@ -20,7 +20,6 @@ import {
Spin,
Toast,
Popconfirm,
Tag,
} from "@douyinfe/semi-ui";
import { toPng, toJpeg, toSvg } from "html-to-image";
import { saveAs } from "file-saver";
@@ -62,6 +61,7 @@ import { IconAddArea, IconAddNote, IconAddTable } from "../../icons";
import LayoutDropdown from "./LayoutDropdown";
import Sidesheet from "./SideSheet/Sidesheet";
import Modal from "./Modal/Modal";
import { useTranslation } from "react-i18next";
export default function ControlPanel({
diagramId,
@@ -100,6 +100,7 @@ export default function ControlPanel({
const { undoStack, redoStack, setUndoStack, setRedoStack } = useUndoRedo();
const { selectedElement, setSelectedElement } = useSelect();
const { transform, setTransform } = useTransform();
const { t } = useTranslation();
const navigate = useNavigate();
const invertLayout = (component) =>
@@ -460,16 +461,12 @@ export default function ControlPanel({
setTransform((prev) => ({ ...prev, zoom: prev.zoom / 1.2 }));
const viewStrictMode = () => {
setSettings((prev) => ({ ...prev, strictMode: !prev.strictMode }));
Toast.success(`Stict mode is ${settings.strictMode ? "on" : "off"}.`);
};
const viewFieldSummary = () => {
setSettings((prev) => ({
...prev,
showFieldSummary: !prev.showFieldSummary,
}));
Toast.success(
`Field summary is ${settings.showFieldSummary ? "off" : "on"}.`,
);
};
const copyAsImage = () => {
toPng(document.getElementById("canvas")).then(function (dataUrl) {
@@ -477,10 +474,10 @@ export default function ControlPanel({
navigator.clipboard
.write([new ClipboardItem({ "image/png": blob })])
.then(() => {
Toast.success("Copied to clipboard.");
Toast.success(t("copied_to_clipboard"));
})
.catch(() => {
Toast.error("Could not copy to clipboard.");
Toast.error(t("oops_smth_went_wrong"));
});
});
};
@@ -607,23 +604,17 @@ export default function ControlPanel({
case ObjectType.TABLE:
navigator.clipboard
.writeText(JSON.stringify({ ...tables[selectedElement.id] }))
.catch(() => {
Toast.error("Could not copy");
});
.catch(() => Toast.error(t("oops_smth_went_wrong")));
break;
case ObjectType.NOTE:
navigator.clipboard
.writeText(JSON.stringify({ ...notes[selectedElement.id] }))
.catch(() => {
Toast.error("Could not copy");
});
.catch(() => Toast.error(t("oops_smth_went_wrong")));
break;
case ObjectType.AREA:
navigator.clipboard
.writeText(JSON.stringify({ ...areas[selectedElement.id] }))
.catch(() => {
Toast.error("Could not copy");
});
.catch(() => Toast.error(t("oops_smth_went_wrong")));
break;
default:
break;
@@ -671,29 +662,29 @@ export default function ControlPanel({
const saveDiagramAs = () => setModal(MODAL.SAVEAS);
const menu = {
File: {
New: {
file: {
new: {
function: () => setModal(MODAL.NEW),
},
"New window": {
new_window: {
function: () => {
const newWindow = window.open("/editor", "_blank");
newWindow.name = window.name;
},
},
Open: {
open: {
function: open,
shortcut: "Ctrl+O",
},
Save: {
save: {
function: save,
shortcut: "Ctrl+S",
},
"Save as": {
save_as: {
function: saveDiagramAs,
shortcut: "Ctrl+Shift+S",
},
"Save as template": {
save_as_template: {
function: () => {
db.templates
.add({
@@ -706,21 +697,20 @@ export default function ControlPanel({
custom: 1,
})
.then(() => {
Toast.success("Template saved!");
Toast.success(t("template_saved"));
});
},
},
Rename: {
rename: {
function: () => {
setModal(MODAL.RENAME);
setPrevTitle(title);
},
},
"Delete diagram": {
delete_diagram: {
warning: {
title: "Delete diagram",
message:
"Are you sure you want to delete this diagram? This operation is irreversible.",
title: t("delete_diagram"),
message: t("are_you_sure_delete_diagram"),
},
function: async () => {
await db.diagrams
@@ -736,17 +726,17 @@ export default function ControlPanel({
setUndoStack([]);
setRedoStack([]);
})
.catch(() => Toast.error("Oops! Something went wrong."));
.catch(() => Toast.error(t("oops_smth_went_wrong")));
},
},
"Import diagram": {
import_diagram: {
function: fileImport,
shortcut: "Ctrl+I",
},
"Import from source": {
import_from_source: {
function: () => setModal(MODAL.IMPORT_SRC),
},
"Export as": {
export_as: {
children: [
{
PNG: () => {
@@ -856,7 +846,7 @@ export default function ControlPanel({
],
function: () => {},
},
"Export source": {
export_source: {
children: [
{
MySQL: () => {
@@ -936,23 +926,27 @@ export default function ControlPanel({
],
function: () => {},
},
Exit: {
exit: {
function: () => {
save();
if (saveState === State.SAVED) navigate("/");
},
},
},
Edit: {
Undo: {
edit: {
undo: {
function: undo,
shortcut: "Ctrl+Z",
},
Redo: {
redo: {
function: redo,
shortcut: "Ctrl+Y",
},
Clear: {
clear: {
warning: {
title: t("clear"),
message: t("are_you_sure_clear"),
},
function: () => {
setTables([]);
setRelationships([]);
@@ -962,57 +956,73 @@ export default function ControlPanel({
setRedoStack([]);
},
},
Edit: {
edit: {
function: edit,
shortcut: "Ctrl+E",
},
Cut: {
cut: {
function: cut,
shortcut: "Ctrl+X",
},
Copy: {
copy: {
function: copy,
shortcut: "Ctrl+C",
},
Paste: {
paste: {
function: paste,
shortcut: "Ctrl+V",
},
Duplicate: {
duplicate: {
function: duplicate,
shortcut: "Ctrl+D",
},
Delete: {
delete: {
function: del,
shortcut: "Del",
},
"Copy as image": {
copy_as_image: {
function: copyAsImage,
shortcut: "Ctrl+Alt+C",
},
},
View: {
Header: {
state: layout.header ? "on" : "off",
view: {
header: {
state: layout.header ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () =>
setLayout((prev) => ({ ...prev, header: !prev.header })),
},
Sidebar: {
state: layout.sidebar ? "on" : "off",
sidebar: {
state: layout.sidebar ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () =>
setLayout((prev) => ({ ...prev, sidebar: !prev.sidebar })),
},
Issues: {
state: layout.issues ? "on" : "off",
issues: {
state: layout.issues ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () =>
setLayout((prev) => ({ ...prev, issues: !prev.issues })),
},
"Strict mode": {
state: settings.strictMode ? "off" : "on",
strict_mode: {
state: settings.strictMode ? (
<i className="bi bi-toggle-off" />
) : (
<i className="bi bi-toggle-on" />
),
function: viewStrictMode,
shortcut: "Ctrl+Shift+M",
},
"Presentation mode": {
presentation_mode: {
function: () => {
setLayout((prev) => ({
...prev,
@@ -1023,32 +1033,44 @@ export default function ControlPanel({
enterFullscreen();
},
},
"Field details": {
state: settings.showFieldSummary ? "on" : "off",
field_details: {
state: settings.showFieldSummary ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: viewFieldSummary,
shortcut: "Ctrl+Shift+F",
},
"Reset view": {
reset_view: {
function: resetView,
shortcut: "Ctrl+R",
},
"Show grid": {
state: settings.showGrid ? "on" : "off",
show_grid: {
state: settings.showGrid ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: viewGrid,
shortcut: "Ctrl+Shift+G",
},
"Show cardinality": {
state: settings.showCardinality ? "on" : "off",
show_cardinality: {
state: settings.showCardinality ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () =>
setSettings((prev) => ({
...prev,
showCardinality: !prev.showCardinality,
})),
},
Theme: {
theme: {
children: [
{
Light: () => {
light: () => {
const body = document.body;
if (body.hasAttribute("theme-mode")) {
body.setAttribute("theme-mode", "light");
@@ -1058,7 +1080,7 @@ export default function ControlPanel({
},
},
{
Dark: () => {
dark: () => {
const body = document.body;
if (body.hasAttribute("theme-mode")) {
body.setAttribute("theme-mode", "dark");
@@ -1070,71 +1092,75 @@ export default function ControlPanel({
],
function: () => {},
},
"Zoom in": {
zoom_in: {
function: zoomIn,
shortcut: "Ctrl+Up/Wheel",
},
"Zoom out": {
zoom_out: {
function: zoomOut,
shortcut: "Ctrl+Down/Wheel",
},
Fullscreen: {
fullscreen: {
function: enterFullscreen,
},
},
Settings: {
"Show timeline": {
settings: {
show_timeline: {
function: () => setSidesheet(SIDESHEET.TIMELINE),
},
Autosave: {
state: settings.autosave ? "on" : "off",
autosave: {
state: settings.autosave ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () =>
setSettings((prev) => {
Toast.success(`Autosave is ${settings.autosave ? "off" : "on"}`);
return { ...prev, autosave: !prev.autosave };
}),
setSettings((prev) => ({ ...prev, autosave: !prev.autosave })),
},
Panning: {
state: settings.panning ? "on" : "off",
panning: {
state: settings.panning ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () =>
setSettings((prev) => {
Toast.success(`Panning is ${settings.panning ? "off" : "on"}`);
return { ...prev, panning: !prev.panning };
}),
setSettings((prev) => ({ ...prev, panning: !prev.panning })),
},
"Table width": {
table_width: {
function: () => setModal(MODAL.TABLE_WIDTH),
},
"Flush storage": {
language: {
function: () => setModal(MODAL.LANGUAGE),
},
flush_storage: {
warning: {
title: "Flush storage",
message:
"Are you sure you want to flush the storage? This will irreversibly delete all your diagrams and custom templates.",
title: t("flush_storage"),
message: t("are_you_sure_flush_storage"),
},
function: async () => {
db.delete()
.then(() => {
Toast.success("Storage flushed");
Toast.success(t("storage_flushed"));
window.location.reload(false);
})
.catch(() => {
Toast.error("Oops! Something went wrong.");
Toast.error(t("oops_smth_went_wrong"));
});
},
},
},
Help: {
Shortcuts: {
help: {
shortcuts: {
function: () => window.open("/shortcuts", "_blank"),
shortcut: "Ctrl+H",
},
"Ask us on discord": {
ask_on_discord: {
function: () => window.open("https://discord.gg/BrjZgNrmR6", "_blank"),
},
"Report a bug": {
report_bug: {
function: () => window.open("/bug-report", "_blank"),
},
"Give feedback": {
feedback: {
function: () => window.open("/survey", "_blank"),
},
},
@@ -1207,7 +1233,7 @@ export default function ControlPanel({
onClick={fitWindow}
style={{ display: "flex", justifyContent: "space-between" }}
>
<div>Fit window / Reset</div>
<div>{t("fit_window_reset")}</div>
<div className="text-gray-400">Ctrl+Alt+W</div>
</Dropdown.Item>
<Dropdown.Divider />
@@ -1225,8 +1251,8 @@ export default function ControlPanel({
<Dropdown.Item>
<InputNumber
field="zoom"
label="Custom zoom"
placeholder="Zoom"
label={t("zoom")}
placeholder={t("zoom")}
suffix={<div className="p-1">%</div>}
onChange={(v) =>
setTransform((prev) => ({
@@ -1249,7 +1275,7 @@ export default function ControlPanel({
</div>
</div>
</Dropdown>
<Tooltip content="Zoom in" position="bottom">
<Tooltip content={t("zoom_in")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded text-lg"
onClick={() =>
@@ -1259,7 +1285,7 @@ export default function ControlPanel({
<i className="fa-solid fa-magnifying-glass-plus" />
</button>
</Tooltip>
<Tooltip content="Zoom out" position="bottom">
<Tooltip content={t("zoom_out")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded text-lg"
onClick={() =>
@@ -1270,7 +1296,7 @@ export default function ControlPanel({
</button>
</Tooltip>
<Divider layout="vertical" margin="8px" />
<Tooltip content="Undo" position="bottom">
<Tooltip content={t("undo")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded flex items-center"
onClick={undo}
@@ -1281,7 +1307,7 @@ export default function ControlPanel({
/>
</button>
</Tooltip>
<Tooltip content="Redo" position="bottom">
<Tooltip content={t("redo")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded flex items-center"
onClick={redo}
@@ -1293,7 +1319,7 @@ export default function ControlPanel({
</button>
</Tooltip>
<Divider layout="vertical" margin="8px" />
<Tooltip content="Add table" position="bottom">
<Tooltip content={t("add_table")} position="bottom">
<button
className="flex items-center py-1 px-2 hover-2 rounded"
onClick={() => addTable()}
@@ -1301,7 +1327,7 @@ export default function ControlPanel({
<IconAddTable />
</button>
</Tooltip>
<Tooltip content="Add subject area" position="bottom">
<Tooltip content={t("add_area")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded flex items-center"
onClick={() => addArea()}
@@ -1309,7 +1335,7 @@ export default function ControlPanel({
<IconAddArea />
</button>
</Tooltip>
<Tooltip content="Add note" position="bottom">
<Tooltip content={t("add_note")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded flex items-center"
onClick={() => addNote()}
@@ -1318,7 +1344,7 @@ export default function ControlPanel({
</button>
</Tooltip>
<Divider layout="vertical" margin="8px" />
<Tooltip content="Save" position="bottom">
<Tooltip content={t("save")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded flex items-center"
onClick={save}
@@ -1326,7 +1352,7 @@ export default function ControlPanel({
<IconSaveStroked size="extra-large" />
</button>
</Tooltip>
<Tooltip content="To-do" position="bottom">
<Tooltip content={t("to_do")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded text-xl -mt-0.5"
onClick={() => setSidesheet(SIDESHEET.TODO)}
@@ -1335,16 +1361,16 @@ export default function ControlPanel({
</button>
</Tooltip>
<Divider layout="vertical" margin="8px" />
<Tooltip content="Change theme" position="bottom">
<Tooltip content={t("theme")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded text-xl -mt-0.5"
onClick={() => {
const body = document.body;
if (body.hasAttribute("theme-mode")) {
if (body.getAttribute("theme-mode") === "light") {
menu["View"]["Theme"].children[1]["Dark"]();
menu["view"]["theme"].children[1]["dark"]();
} else {
menu["View"]["Theme"].children[0]["Light"]();
menu["view"]["theme"].children[0]["light"]();
}
}
}}
@@ -1366,15 +1392,15 @@ export default function ControlPanel({
function getState() {
switch (saveState) {
case State.NONE:
return "No changes";
return t("no_changes");
case State.LOADING:
return "Loading . . .";
return t("loading");
case State.SAVED:
return `Last saved ${lastSaved}`;
return `${t("last_saved")} ${lastSaved}`;
case State.SAVING:
return "Saving . . .";
return t("saving");
case State.ERROR:
return "Failed to save";
return t("failed_to_save");
default:
return "";
}
@@ -1429,7 +1455,7 @@ export default function ControlPanel({
key={i}
onClick={Object.values(e)[0]}
>
{Object.keys(e)[0]}
{t(Object.keys(e)[0])}
</Dropdown.Item>
),
)}
@@ -1444,7 +1470,7 @@ export default function ControlPanel({
}}
onClick={menu[category][item].function}
>
{item}
{t(item)}
<IconChevronRight />
</Dropdown.Item>
</Dropdown>
@@ -1458,8 +1484,10 @@ export default function ControlPanel({
content={menu[category][item].warning.message}
onConfirm={menu[category][item].function}
position="right"
okText={t("confirm")}
cancelText={t("cancel")}
>
<Dropdown.Item>{item}</Dropdown.Item>
<Dropdown.Item>{t(item)}</Dropdown.Item>
</Popconfirm>
);
}
@@ -1476,18 +1504,15 @@ export default function ControlPanel({
}
>
<div className="w-full flex items-center justify-between">
<div>{item}</div>
<div>{t(item)}</div>
<div className="flex items-center gap-1">
{menu[category][item].shortcut && (
<div className="text-gray-400">
{menu[category][item].shortcut}
</div>
)}
{menu[category][item].state && (
<Tag color="blue">
{menu[category][item].state}
</Tag>
)}
{menu[category][item].state &&
menu[category][item].state}
</div>
</div>
</Dropdown.Item>
@@ -1496,7 +1521,9 @@ export default function ControlPanel({
</Dropdown.Menu>
}
>
<div className="px-3 py-1 hover-2 rounded">{category}</div>
<div className="px-3 py-1 hover-2 rounded">
{t(category)}
</div>
</Dropdown>
))}
</div>

View File

@@ -6,9 +6,12 @@ import {
import { Dropdown } from "@douyinfe/semi-ui";
import { useLayout } from "../../hooks";
import { enterFullscreen, exitFullscreen } from "../../utils/fullscreen";
import { useTranslation } from "react-i18next";
export default function LayoutDropdown() {
const { layout, setLayout } = useLayout();
const { t } = useTranslation();
const invertLayout = (component) =>
setLayout((prev) => ({ ...prev, [component]: !prev[component] }));
@@ -24,7 +27,7 @@ export default function LayoutDropdown() {
}
onClick={() => invertLayout("header")}
>
Header
{t("header")}
</Dropdown.Item>
<Dropdown.Item
icon={
@@ -32,7 +35,7 @@ export default function LayoutDropdown() {
}
onClick={() => invertLayout("sidebar")}
>
Sidebar
{t("sidebar")}
</Dropdown.Item>
<Dropdown.Item
icon={
@@ -40,7 +43,7 @@ export default function LayoutDropdown() {
}
onClick={() => invertLayout("issues")}
>
Issues
{t("issues")}
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
@@ -54,7 +57,7 @@ export default function LayoutDropdown() {
invertLayout("fullscreen");
}}
>
Fullscreen
{t("fullscreen")}
</Dropdown.Item>
</Dropdown.Menu>
}

View File

@@ -5,11 +5,13 @@ import {
import { Upload, Banner } from "@douyinfe/semi-ui";
import { STATUS } from "../../../data/constants";
import { useAreas, useNotes, useTables } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function ImportDiagram({ setImportData, error, setError }) {
const { areas } = useAreas();
const { notes } = useNotes();
const { tables, relationships } = useTables();
const { t } = useTranslation();
const diagramIsEmpty = () => {
return (
@@ -84,8 +86,8 @@ export default function ImportDiagram({ setImportData, error, setError }) {
};
}}
draggable={true}
dragMainText="Drag and drop the file here or click to upload."
dragSubText="Support json and ddb"
dragMainText={t("drag_and_drop_files")}
dragSubText={t("support_json_and_ddb")}
accept="application/json,.ddb"
onRemove={() =>
setError({

View File

@@ -1,5 +1,6 @@
import { Upload, Checkbox, Banner } from "@douyinfe/semi-ui";
import { STATUS } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function ImportSource({
importData,
@@ -7,6 +8,8 @@ export default function ImportSource({
error,
setError,
}) {
const { t } = useTranslation();
return (
<div>
<Upload
@@ -30,8 +33,8 @@ export default function ImportSource({
};
}}
draggable={true}
dragMainText="Drag and drop the file here or click to upload."
dragSubText="Upload an sql file to autogenerate your tables and columns."
dragMainText={t("drag_and_drop_files")}
dragSubText={t("upload_sql_to_generate_diagrams")}
accept=".sql"
onRemove={() => {
setError({
@@ -50,7 +53,7 @@ export default function ImportSource({
/>
<div>
<div className="text-xs mb-3 mt-1 opacity-80">
* For the time being loading only MySQL scripts is supported.
{t("only_mysql_supported")}
</div>
<Checkbox
aria-label="overwrite checkbox"
@@ -63,7 +66,7 @@ export default function ImportSource({
}))
}
>
Overwrite existing diagram
{t("overwrite_existing_diagram")}
</Checkbox>
<div className="mt-2">
{error.type === STATUS.ERROR ? (

View File

@@ -0,0 +1,30 @@
import { useTranslation } from "react-i18next";
import { useSettings } from "../../../hooks";
import { languages } from "../../../i18n/i18n";
export default function Language() {
const { settings } = useSettings();
const { i18n } = useTranslation();
return (
<div className="grid grid-cols-3 gap-4">
{languages.map((l) => (
<button
key={l.code}
onClick={() => i18n.changeLanguage(l.code)}
className={`space-y-1 py-3 px-4 rounded-md border-2 ${
settings.mode === "dark"
? "bg-zinc-700 hover:bg-zinc-600"
: "bg-zinc-100 hover:bg-zinc-200"
} ${i18n.resolvedLanguage === l.code ? "border-zinc-400" : "border-transparent"}`}
>
<div className="flex justify-between items-center">
<div className="font-semibold">{l.native_name}</div>
<div className="opacity-60">{l.code}</div>
</div>
<div className="text-start">{l.name}</div>
</button>
))}
</div>
);
}

View File

@@ -27,11 +27,13 @@ import New from "./New";
import ImportDiagram from "./ImportDiagram";
import ImportSource from "./ImportSource";
import SetTableWidth from "./SetTableWidth";
import Language from "./Language";
import CodeMirror from "@uiw/react-codemirror";
import { sql } from "@codemirror/lang-sql";
import { vscodeDark } from "@uiw/codemirror-theme-vscode";
import { json } from "@codemirror/lang-json";
import { githubLight } from "@uiw/codemirror-theme-github";
import { useTranslation } from "react-i18next";
const languageExtension = {
sql: [sql()],
@@ -49,6 +51,7 @@ export default function Modal({
exportData,
setExportData,
}) {
const { t } = useTranslation();
const { setTables, setRelationships } = useTables();
const { setNotes } = useNotes();
const { setAreas } = useAreas();
@@ -239,7 +242,7 @@ export default function Modal({
case MODAL.SAVEAS:
return (
<Input
placeholder="Diagram name"
placeholder={t("name")}
value={saveAsTitle}
onChange={(v) => setSaveAsTitle(v)}
/>
@@ -261,10 +264,10 @@ export default function Modal({
theme={settings.mode === "dark" ? vscodeDark : githubLight}
/>
)}
<div className="text-sm font-semibold mt-2">Filename:</div>
<div className="text-sm font-semibold mt-2">{t("filename")}:</div>
<Input
value={exportData.filename}
placeholder="Filename"
placeholder={t("filename")}
suffix={<div className="p-2">{`.${exportData.extension}`}</div>}
onChange={(value) =>
setExportData((prev) => ({ ...prev, filename: value }))
@@ -276,12 +279,14 @@ export default function Modal({
} else {
return (
<div className="text-center my-3">
<Spin tip="Loading..." size="large" />
<Spin tip={t("loading")} size="large" />
</div>
);
}
case MODAL.TABLE_WIDTH:
return <SetTableWidth />;
case MODAL.LANGUAGE:
return <Language />;
default:
return <></>;
}
@@ -326,8 +331,9 @@ export default function Modal({
(modal === MODAL.SAVEAS && saveAsTitle === "") ||
(modal === MODAL.IMPORT_SRC && importSource.src === ""),
}}
cancelText="Cancel"
cancelText={t("cancel")}
width={modal === MODAL.NEW ? 740 : 600}
bodyStyle={{ maxHeight: window.innerHeight - 280, overflow: "auto" }}
>
{getModalBody()}
</SemiUIModal>

View File

@@ -2,13 +2,15 @@ import { db } from "../../../data/db";
import { useSettings } from "../../../hooks";
import { useLiveQuery } from "dexie-react-hooks";
import Thumbnail from "../../Thumbnail";
import { useTranslation } from "react-i18next";
export default function New({ selectedTemplateId, setSelectedTemplateId }) {
const { settings } = useSettings();
const { t } = useTranslation();
const templates = useLiveQuery(() => db.templates.toArray());
return (
<div className="h-[360px] grid grid-cols-3 gap-2 overflow-auto px-1">
<div className="grid grid-cols-3 gap-2 overflow-auto px-1">
<div onClick={() => setSelectedTemplateId(0)}>
<div
className={`rounded-md h-[180px] border-2 hover:border-dashed ${
@@ -17,7 +19,7 @@ export default function New({ selectedTemplateId, setSelectedTemplateId }) {
>
<Thumbnail i={0} diagram={{}} zoom={0.24} theme={settings.mode} />
</div>
<div className="text-center mt-1">Blank</div>
<div className="text-center mt-1">{t("blank")}</div>
</div>
{templates?.map((temp, i) => (
<div key={i} onClick={() => setSelectedTemplateId(temp.id)}>

View File

@@ -1,9 +1,11 @@
import { db } from "../../../data/db";
import { Banner } from "@douyinfe/semi-ui";
import { useLiveQuery } from "dexie-react-hooks";
import { useTranslation } from "react-i18next";
export default function Open({ selectedDiagramId, setSelectedDiagramId }) {
const diagrams = useLiveQuery(() => db.diagrams.toArray());
const { t } = useTranslation();
const getDiagramSize = (d) => {
const size = JSON.stringify(d).length;
@@ -32,9 +34,9 @@ export default function Open({ selectedDiagramId, setSelectedDiagramId }) {
<table className="w-full text-left border-separate border-spacing-x-0">
<thead>
<tr>
<th>Name</th>
<th>Last Modified</th>
<th>Size</th>
<th>{t("name")}</th>
<th>{t("last_modified")}</th>
<th>{t("size")}</th>
</tr>
</thead>
<tbody>

View File

@@ -1,9 +1,12 @@
import { Input } from "@douyinfe/semi-ui";
import { useTranslation } from "react-i18next";
export default function Rename({ title, setTitle }) {
const { t } = useTranslation();
return (
<Input
placeholder="Diagram name"
placeholder={t("name")}
value={title}
onChange={(v) => setTitle(v)}
/>

View File

@@ -6,8 +6,10 @@ import timeLineDark from "../../../assets/process_dark.png";
import todo from "../../../assets/calendar.png";
import Timeline from "./Timeline";
import Todo from "./Todo";
import { useTranslation } from "react-i18next";
export default function Sidesheet({ type, onClose }) {
const { t } = useTranslation();
const { settings } = useSettings();
function getTitle(type) {
@@ -20,14 +22,14 @@ export default function Sidesheet({ type, onClose }) {
className="w-7"
alt="chat icon"
/>
<div className="ms-3 text-lg">Timeline</div>
<div className="ms-3 text-lg">{t("timeline")}</div>
</div>
);
case SIDESHEET.TODO:
return (
<div className="flex items-center">
<img src={todo} className="w-7" alt="todo icon" />
<div className="ms-3 text-lg">To-do list</div>
<div className="ms-3 text-lg">{t("to_do")}</div>
</div>
);
default:

View File

@@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
import { useUndoRedo } from "../../../hooks";
import { List } from "@douyinfe/semi-ui";
export default function Timeline() {
const { undoStack } = useUndoRedo();
const { t } = useTranslation();
if (undoStack.length > 0) {
return (
@@ -22,11 +24,6 @@ export default function Timeline() {
</List>
);
} else {
return (
<div className="m-5 sidesheet-theme">
No activity was recorded. You have not added anything to your diagram
yet.
</div>
);
return <div className="m-5 sidesheet-theme">{t("no_activity")}</div>;
}
}

View File

@@ -21,6 +21,7 @@ import {
} from "@douyinfe/semi-icons";
import { State } from "../../../data/constants";
import { useTasks, useSaveState } from "../../../hooks";
import { useTranslation } from "react-i18next";
const Priority = {
NONE: 0,
@@ -30,10 +31,10 @@ const Priority = {
};
const SortOrder = {
ORIGINAL: "My order",
PRIORITY: "Priority",
COMPLETED: "Completed",
ALPHABETICALLY: "Alphabetically",
ORIGINAL: "my_order",
PRIORITY: "priority",
COMPLETED: "completed",
ALPHABETICALLY: "alphabetically",
};
export default function Todo() {
@@ -41,17 +42,18 @@ export default function Todo() {
const [, setSortOrder] = useState(SortOrder.ORIGINAL);
const { tasks, setTasks, updateTask } = useTasks();
const { setSaveState } = useSaveState();
const { t } = useTranslation();
const priorityLabel = (p) => {
switch (p) {
case Priority.NONE:
return "None";
return t("none");
case Priority.LOW:
return "Low";
return t("low");
case Priority.MEDIUM:
return "Medium";
return t("medium");
case Priority.HIGH:
return "High";
return t("high");
default:
return "";
}
@@ -91,7 +93,7 @@ export default function Todo() {
} else {
return 0;
}
})
}),
);
break;
case SortOrder.ALPHABETICALLY:
@@ -116,7 +118,7 @@ export default function Todo() {
sort(order);
}}
>
{order}
{t(order)}
</Dropdown.Item>
))}
</Dropdown.Menu>
@@ -128,7 +130,7 @@ export default function Todo() {
theme="borderless"
type="tertiary"
>
Sort by <IconCaretdown />
{t("sort_by")} <IconCaretdown />
</Button>
</Dropdown>
<Button
@@ -147,12 +149,12 @@ export default function Todo() {
]);
}}
>
Add task
{t("add_task")}
</Button>
</div>
{tasks.length > 0 ? (
<List className="sidesheet-theme">
{tasks.map((t, i) => (
{tasks.map((task, i) => (
<List.Item
key={i}
style={{ paddingLeft: "18px", paddingRight: "18px" }}
@@ -163,7 +165,7 @@ export default function Todo() {
<Row gutter={6} align="middle" type="flex" className="mb-2">
<Col span={2}>
<Checkbox
checked={t.complete}
checked={task.complete}
onChange={(e) => {
updateTask(i, { complete: e.target.checked });
setSaveState(State.SAVING);
@@ -172,25 +174,25 @@ export default function Todo() {
</Col>
<Col span={19}>
<Input
placeholder="Title"
placeholder={t("title")}
onChange={(v) => updateTask(i, { title: v })}
value={t.title}
value={task.title}
onBlur={() => setSaveState(State.SAVING)}
></Input>
/>
</Col>
<Col span={3}>
<Popover
content={
<div className="p-2 popover-theme">
<div className="mb-2 font-semibold">
Set priority:
{t("priority")}:
</div>
<RadioGroup
onChange={(e) => {
updateTask(i, { priority: e.target.value });
setSaveState(State.SAVING);
}}
value={t.priority}
value={task.priority}
direction="vertical"
>
<Radio value={Priority.NONE}>
@@ -221,12 +223,12 @@ export default function Todo() {
style={{ marginTop: "12px" }}
onClick={() => {
setTasks((prev) =>
prev.filter((task, j) => i !== j)
prev.filter((_, j) => i !== j),
);
setSaveState(State.SAVING);
}}
>
Delete
{t("delete")}
</Button>
</div>
}
@@ -243,7 +245,7 @@ export default function Todo() {
<Col span={2}></Col>
<Col span={22}>
<TextArea
placeholder="Details"
placeholder={t("details")}
onChange={(v) => updateTask(i, { details: v })}
value={t.details}
onBlur={() => setSaveState(State.SAVING)}
@@ -254,9 +256,9 @@ export default function Todo() {
<Row>
<Col span={2}></Col>
<Col span={22}>
Priority:{" "}
<Tag color={priorityColor(t.priority)}>
{priorityLabel(t.priority)}
{t("priority")}:{" "}
<Tag color={priorityColor(task.priority)}>
{priorityLabel(task.priority)}
</Tag>
</Col>
</Row>
@@ -265,10 +267,7 @@ export default function Todo() {
))}
</List>
) : (
<div className="m-5 sidesheet-theme">
You have no tasks yet. Add your to-dos and keep track of your
progress.
</div>
<div className="m-5 sidesheet-theme">{t("no_tasks")}</div>
)}
</>
);

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { Row, Col, Button, Input, Popover, Toast } from "@douyinfe/semi-ui";
import { Row, Col, Button, Input, Popover } from "@douyinfe/semi-ui";
import { IconDeleteStroked } from "@douyinfe/semi-icons";
import { useAreas, useSaveState, useUndoRedo } from "../../../hooks";
import {
@@ -8,9 +8,11 @@ import {
State,
defaultBlue,
} from "../../../data/constants";
import ColorPalette from "../../ColorPalette";
import ColorPalette from "../../ColorPicker";
import { useTranslation } from "react-i18next";
export default function AreaInfo({ data, i }) {
const { t } = useTranslation();
const { setSaveState } = useSaveState();
const { deleteArea, updateArea } = useAreas();
const { setUndoStack, setRedoStack } = useUndoRedo();
@@ -28,7 +30,7 @@ export default function AreaInfo({ data, i }) {
<Col span={18}>
<Input
value={data.name}
placeholder="Name"
placeholder={t("name")}
onChange={(value) => updateArea(data.id, { name: value })}
onFocus={(e) => setEditField({ name: e.target.value })}
onBlur={(e) => {
@@ -41,7 +43,10 @@ export default function AreaInfo({ data, i }) {
aid: i,
undo: editField,
redo: { name: e.target.value },
message: `Edit area name to ${e.target.value}`,
message: t("edit_area", {
areaName: e.target.value,
extra: "[name]",
}),
},
]);
setRedoStack([]);
@@ -67,7 +72,10 @@ export default function AreaInfo({ data, i }) {
aid: i,
undo: { color: data.color },
redo: { color: c },
message: `Edit area color to ${c}`,
message: t("edit_area", {
areaName: data.name,
extra: "[color]",
}),
},
]);
setRedoStack([]);
@@ -90,10 +98,7 @@ export default function AreaInfo({ data, i }) {
<Button
icon={<IconDeleteStroked />}
type="danger"
onClick={() => {
Toast.success(`Area deleted!`);
deleteArea(i);
}}
onClick={() => deleteArea(i, true)}
/>
</Col>
</Row>

View File

@@ -1,29 +1,29 @@
import { Row, Col, Button } from "@douyinfe/semi-ui";
import { Button } from "@douyinfe/semi-ui";
import { IconPlus } from "@douyinfe/semi-icons";
import Empty from "../Empty";
import { useAreas } from "../../../hooks";
import SearchBar from "./SearchBar";
import AreaInfo from "./AreaDetails";
import { useTranslation } from "react-i18next";
export default function AreasTab() {
const { areas, addArea } = useAreas();
const { t } = useTranslation();
return (
<div>
<Row gutter={6}>
<Col span={16}>
<SearchBar />
</Col>
<Col span={8}>
<Button icon={<IconPlus />} block onClick={() => addArea()}>
Add area
<div className="flex gap-2">
<SearchBar />
<div>
<Button icon={<IconPlus />} block onClick={addArea}>
{t("add_area")}
</Button>
</Col>
</Row>
</div>
</div>
{areas.length <= 0 ? (
<Empty
title="No subject areas"
text="Add subject areas to organize tables!"
title={t("no_subject_areas")}
text={t("no_subject_areas_text")}
/>
) : (
<div className="p-2">

View File

@@ -2,18 +2,20 @@ import { useState } from "react";
import { useAreas } from "../../../hooks";
import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons";
import { useTranslation } from "react-i18next";
export default function SearchBar() {
const { areas } = useAreas();
const [searchText, setSearchText] = useState("");
const { t } = useTranslation();
const [filteredResult, setFilteredResult] = useState(
areas.map((t) => t.name)
areas.map((t) => t.name),
);
const handleStringSearch = (value) => {
setFilteredResult(
areas.map((t) => t.name).filter((i) => i.includes(value))
areas.map((t) => t.name).filter((i) => i.includes(value)),
);
};
@@ -23,8 +25,8 @@ export default function SearchBar() {
value={searchText}
showClear
prefix={<IconSearch />}
placeholder="Search..."
emptyContent={<div className="p-3 popover-theme">No areas found</div>}
placeholder={t("search")}
emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setSearchText(v)}
onSelect={(v) => {

View File

@@ -3,10 +3,12 @@ import { Collapse, Badge } from "@douyinfe/semi-ui";
import { arrayIsEqual } from "../../utils/utils";
import { getIssues } from "../../utils/issues";
import { useSettings, useTables, useTypes } from "../../hooks";
import { useTranslation } from "react-i18next";
export default function Issues() {
const { settings } = useSettings();
const { types } = useTypes();
const { t } = useTranslation();
const { settings } = useSettings();
const { tables, relationships } = useTables();
const [issues, setIssues] = useState([]);
@@ -38,7 +40,7 @@ export default function Issues() {
>
<div className="pe-3 select-none">
<i className="fa-solid fa-triangle-exclamation me-2 text-yellow-500" />
Issues
{t("issues")}
</div>
</Badge>
}
@@ -46,9 +48,7 @@ export default function Issues() {
>
<div className="max-h-[160px] overflow-y-auto">
{settings.strictMode ? (
<div className="mb-1">
Strict mode is off so no issues will be displayed.
</div>
<div className="mb-1">{t("strict_mode_is_on_no_issues")}</div>
) : issues.length > 0 ? (
<>
{issues.map((e, i) => (
@@ -58,7 +58,7 @@ export default function Issues() {
))}
</>
) : (
<div>No issues were detected.</div>
<div>{t("no_issues")}</div>
)}
</div>
</Collapse.Panel>

View File

@@ -1,20 +1,15 @@
import { useState } from "react";
import {
Button,
Collapse,
TextArea,
Popover,
Input,
Toast,
} from "@douyinfe/semi-ui";
import { Button, Collapse, TextArea, Popover, Input } from "@douyinfe/semi-ui";
import { IconDeleteStroked, IconCheckboxTick } from "@douyinfe/semi-icons";
import { noteThemes, Action, ObjectType } from "../../../data/constants";
import { useNotes, useUndoRedo } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function NoteInfo({ data, nid }) {
const { updateNote, deleteNote } = useNotes();
const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({});
const { t } = useTranslation();
return (
<Collapse.Panel
@@ -27,10 +22,10 @@ export default function NoteInfo({ data, nid }) {
id={`scroll_note_${data.id}`}
>
<div className="flex items-center mb-2">
<div className="font-semibold me-2">Title:</div>
<div className="font-semibold me-2 break-keep">{t("title")}:</div>
<Input
value={data.title}
placeholder="Title"
placeholder={t("title")}
onChange={(value) => updateNote(data.id, { title: value })}
onFocus={(e) => setEditField({ title: e.target.value })}
onBlur={(e) => {
@@ -43,7 +38,10 @@ export default function NoteInfo({ data, nid }) {
nid: data.id,
undo: editField,
redo: { title: e.target.value },
message: `Edit note title to "${e.target.name}"`,
message: t("edit_note", {
noteTitle: e.target.value,
extra: "[title]",
}),
},
]);
setRedoStack([]);
@@ -52,7 +50,7 @@ export default function NoteInfo({ data, nid }) {
</div>
<div className="flex justify-between align-top">
<TextArea
placeholder="Add content"
placeholder={t("content")}
value={data.content}
autosize
onChange={(value) => {
@@ -79,7 +77,10 @@ export default function NoteInfo({ data, nid }) {
nid: nid,
undo: editField,
redo: { content: e.target.value, height: newHeight },
message: `Edit note content to "${e.target.value}"`,
message: t("edit_note", {
noteTitle: e.target.value,
extra: "[content]",
}),
},
]);
setRedoStack([]);
@@ -90,7 +91,7 @@ export default function NoteInfo({ data, nid }) {
<Popover
content={
<div className="popover-theme">
<div className="font-medium mb-1">Theme</div>
<div className="font-medium mb-1">{t("theme")}</div>
<hr />
<div className="py-3">
{noteThemes.map((c) => (
@@ -107,7 +108,10 @@ export default function NoteInfo({ data, nid }) {
nid: nid,
undo: { color: data.color },
redo: { color: c },
message: `Edit note color to ${c}`,
message: t("edit_note", {
noteTitle: data.title,
extra: "[color]",
}),
},
]);
setRedoStack([]);
@@ -136,10 +140,7 @@ export default function NoteInfo({ data, nid }) {
<Button
icon={<IconDeleteStroked />}
type="danger"
onClick={() => {
Toast.success(`Note deleted!`);
deleteNote(nid);
}}
onClick={() => deleteNote(nid, true)}
/>
</div>
</div>

View File

@@ -1,35 +1,35 @@
import { Row, Col, Button, Collapse } from "@douyinfe/semi-ui";
import { Button, Collapse } from "@douyinfe/semi-ui";
import { IconPlus } from "@douyinfe/semi-icons";
import { useNotes, useSelect } from "../../../hooks";
import Empty from "../Empty";
import SearchBar from "./SearchBar";
import NoteInfo from "./NoteInfo";
import { useTranslation } from "react-i18next";
export default function NotesTab() {
const { notes, addNote } = useNotes();
const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
return (
<>
<Row gutter={6}>
<Col span={16}>
<SearchBar
setActiveKey={(activeKey) =>
setSelectedElement((prev) => ({
...prev,
id: parseInt(activeKey),
}))
}
/>
</Col>
<Col span={8}>
<div className="flex gap-2">
<SearchBar
setActiveKey={(activeKey) =>
setSelectedElement((prev) => ({
...prev,
id: parseInt(activeKey),
}))
}
/>
<div>
<Button icon={<IconPlus />} block onClick={() => addNote()}>
Add note
{t("add_note")}
</Button>
</Col>
</Row>
</div>
</div>
{notes.length <= 0 ? (
<Empty title="No text notes" text="Add notes cuz why not!" />
<Empty title={t("no_notes")} text={t("no_notes_text")} />
) : (
<Collapse
activeKey={selectedElement.open ? `${selectedElement.id}` : ""}

View File

@@ -2,17 +2,20 @@ import { useState } from "react";
import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons";
import { useNotes } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function SearchBar({ setActiveKey }) {
const { notes } = useNotes();
const [searchText, setSearchText] = useState("");
const { t } = useTranslation();
const [filteredResult, setFilteredResult] = useState(
notes.map((t) => t.title)
notes.map((t) => t.title),
);
const handleStringSearch = (value) => {
setFilteredResult(
notes.map((t) => t.title).filter((i) => i.includes(value))
notes.map((t) => t.title).filter((i) => i.includes(value)),
);
};
@@ -22,8 +25,8 @@ export default function SearchBar({ setActiveKey }) {
value={searchText}
showClear
prefix={<IconSearch />}
placeholder="Search..."
emptyContent={<div className="p-3 popover-theme">No notes found</div>}
placeholder={t("search")}
emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setSearchText(v)}
onSelect={(v) => {

View File

@@ -19,14 +19,16 @@ import {
ObjectType,
} from "../../../data/constants";
import { useTables, useUndoRedo } from "../../../hooks";
import i18n from "../../../i18n/i18n";
import { useTranslation } from "react-i18next";
const columns = [
{
title: "Primary",
title: i18n.t("primary"),
dataIndex: "primary",
},
{
title: "Foreign",
title: i18n.t("foreign"),
dataIndex: "foreign",
},
];
@@ -34,6 +36,7 @@ const columns = [
export default function RelationshipInfo({ data }) {
const { setUndoStack, setRedoStack } = useUndoRedo();
const { tables, setRelationships, deleteRelationship } = useTables();
const { t } = useTranslation();
const swapKeys = () => {
setUndoStack((prev) => [
@@ -54,7 +57,10 @@ export default function RelationshipInfo({ data }) {
endTableId: data.startTableId,
endFieldId: data.startFieldId,
},
message: `Swap primary and foreign tables`,
message: t("edit_relationship", {
refName: data.name,
extra: "[swap keys]",
}),
},
]);
setRedoStack([]);
@@ -71,8 +77,8 @@ export default function RelationshipInfo({ data }) {
endTableId: e.startTableId,
endFieldId: e.startFieldId,
}
: e
)
: e,
),
);
};
@@ -85,12 +91,17 @@ export default function RelationshipInfo({ data }) {
rid: data.id,
undo: { cardinality: data.cardinality },
redo: { cardinality: value },
message: `Edit relationship cardinality`,
message: t("edit_relationship", {
refName: data.name,
extra: "[cardinality]",
}),
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) => (idx === data.id ? { ...e, cardinality: value } : e))
prev.map((e, idx) =>
idx === data.id ? { ...e, cardinality: value } : e,
),
);
};
@@ -102,7 +113,10 @@ export default function RelationshipInfo({ data }) {
rid: data.id,
undo: { [undoKey]: data[undoKey] },
redo: { [undoKey]: value },
message: `Edit relationship ${key} constraint`,
message: t("edit_relationship", {
refName: data.name,
extra: "[constraint]",
}),
});
setUndoStack((prev) => [
...prev,
@@ -112,12 +126,15 @@ export default function RelationshipInfo({ data }) {
rid: data.id,
undo: { [undoKey]: data[undoKey] },
redo: { [undoKey]: value },
message: `Edit relationship ${key} constraint`,
message: t("edit_relationship", {
refName: data.name,
extra: "[constraint]",
}),
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) => (idx === data.id ? { ...e, [undoKey]: value } : e))
prev.map((e, idx) => (idx === data.id ? { ...e, [undoKey]: value } : e)),
);
};
@@ -133,11 +150,11 @@ export default function RelationshipInfo({ data }) {
>
<div className="flex justify-between items-center mb-3">
<div className="me-3">
<span className="font-semibold">Primary: </span>
<span className="font-semibold">{t("primary")}: </span>
{tables[data.endTableId].name}
</div>
<div className="mx-1">
<span className="font-semibold">Foreign: </span>
<span className="font-semibold">{t("foreign")}: </span>
{tables[data.startTableId].name}
</div>
<div className="ms-1">
@@ -168,7 +185,7 @@ export default function RelationshipInfo({ data }) {
block
onClick={swapKeys}
>
Swap
{t("swap")}
</Button>
</div>
</div>
@@ -181,7 +198,7 @@ export default function RelationshipInfo({ data }) {
</Popover>
</div>
</div>
<div className="font-semibold my-1">Cardinality</div>
<div className="font-semibold my-1">{t("cardinality")}:</div>
<Select
optionList={Object.values(Cardinality).map((v) => ({
label: v,
@@ -193,7 +210,7 @@ export default function RelationshipInfo({ data }) {
/>
<Row gutter={6} className="my-3">
<Col span={12}>
<div className="font-semibold">On update: </div>
<div className="font-semibold">{t("on_update")}: </div>
<Select
optionList={Object.values(Constraint).map((v) => ({
label: v,
@@ -205,7 +222,7 @@ export default function RelationshipInfo({ data }) {
/>
</Col>
<Col span={12}>
<div className="font-semibold">On delete: </div>
<div className="font-semibold">{t("on_delete")}: </div>
<Select
optionList={Object.values(Constraint).map((v) => ({
label: v,
@@ -223,7 +240,7 @@ export default function RelationshipInfo({ data }) {
type="danger"
onClick={() => deleteRelationship(data.id)}
>
Delete
{t("delete")}
</Button>
</Collapse.Panel>
</div>

View File

@@ -4,10 +4,12 @@ import Empty from "../Empty";
import SearchBar from "./SearchBar";
import RelationshipInfo from "./RelationshipInfo";
import { ObjectType } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function RelationshipsTab() {
const { relationships } = useTables();
const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
return (
<>
@@ -33,8 +35,8 @@ export default function RelationshipsTab() {
>
{relationships.length <= 0 ? (
<Empty
title="No relationships"
text="Drag to connect fields and form relationships!"
title={t("no_relationships")}
text={t("no_relationships_text")}
/>
) : (
relationships.map((r) => <RelationshipInfo key={r.id} data={r} />)

View File

@@ -3,11 +3,14 @@ import { useSelect, useTables } from "../../../hooks";
import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons";
import { ObjectType } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function SearchBar() {
const { relationships } = useTables();
const [searchText, setSearchText] = useState("");
const { setSelectedElement } = useSelect();
const { t } = useTranslation();
const [filteredResult, setFilteredResult] = useState(
relationships.map((t) => t.name),
);
@@ -24,10 +27,8 @@ export default function SearchBar() {
value={searchText}
showClear
prefix={<IconSearch />}
placeholder="Search..."
emptyContent={
<div className="p-3 popover-theme">No relationships found</div>
}
placeholder={t("search")}
emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setSearchText(v)}
onSelect={(v) => {

View File

@@ -7,17 +7,23 @@ import Issues from "./Issues";
import AreasTab from "./AreasTab/AreasTab";
import NotesTab from "./NotesTab/NotesTab";
import TablesTab from "./TablesTab/TablesTab";
import { useTranslation } from "react-i18next";
export default function SidePanel({ width, resize, setResize }) {
const { layout } = useLayout();
const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
const tabList = [
{ tab: "Tables", itemKey: Tab.TABLES, component: <TablesTab /> },
{ tab: "Relationships", itemKey: Tab.RELATIONSHIPS, component: <RelationshipsTab /> },
{ tab: "Subject Areas", itemKey: Tab.AREAS, component: <AreasTab /> },
{ tab: "Notes", itemKey: Tab.NOTES, component: <NotesTab /> },
{ tab: "Types", itemKey: Tab.TYPES, component: <TypesTab /> },
{ tab: t("tables"), itemKey: Tab.TABLES, component: <TablesTab /> },
{
tab: t("relationships"),
itemKey: Tab.RELATIONSHIPS,
component: <RelationshipsTab />,
},
{ tab: t("subject_areas"), itemKey: Tab.AREAS, component: <AreasTab /> },
{ tab: t("notes"), itemKey: Tab.NOTES, component: <NotesTab /> },
{ tab: t("types"), itemKey: Tab.TYPES, component: <TypesTab /> },
];
return (
@@ -36,13 +42,12 @@ export default function SidePanel({ width, resize, setResize }) {
}
collapsible
>
{tabList.length && tabList.map(tab =>
<TabPane tab={tab.tab} itemKey={tab.itemKey} key={tab.itemKey}>
<div className="p-2">
{tab.component}
</div>
</TabPane>
)}
{tabList.length &&
tabList.map((tab) => (
<TabPane tab={tab.tab} itemKey={tab.itemKey} key={tab.itemKey}>
<div className="p-2">{tab.component}</div>
</TabPane>
))}
</Tabs>
</div>
{layout.issues && (

View File

@@ -11,18 +11,21 @@ import { Action, ObjectType } from "../../../data/constants";
import { IconDeleteStroked } from "@douyinfe/semi-icons";
import { hasCheck, hasPrecision, isSized } from "../../../utils/toSQL";
import { useTables, useUndoRedo } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function FieldDetails({ data, tid, index }) {
const { t } = useTranslation();
const { tables } = useTables();
const { setUndoStack, setRedoStack } = useUndoRedo();
const { updateField, deleteField } = useTables();
const [editField, setEditField] = useState({});
return (
<div>
<div className="font-semibold">Default value</div>
<div className="font-semibold">{t("default_value")}</div>
<Input
className="my-2"
placeholder="Set default"
placeholder={t("default_value")}
value={data.default}
disabled={
data.type === "BLOB" ||
@@ -45,7 +48,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index,
undo: editField,
redo: { default: e.target.value },
message: `Edit table field default to ${e.target.value}`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -53,7 +59,9 @@ export default function FieldDetails({ data, tid, index }) {
/>
{(data.type === "ENUM" || data.type === "SET") && (
<>
<div className="font-semibold mb-1">{data.type} values</div>
<div className="font-semibold mb-1">
{data.type} {t("values")}
</div>
<TagInput
separator={[",", ", ", " ,"]}
value={data.values}
@@ -62,7 +70,7 @@ export default function FieldDetails({ data, tid, index }) {
}
addOnBlur
className="my-2"
placeholder="Use ',' for batch input"
placeholder={t("use_for_batch_input")}
onChange={(v) => updateField(tid, index, { values: v })}
onFocus={() => setEditField({ values: data.values })}
onBlur={() => {
@@ -80,9 +88,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index,
undo: editField,
redo: { values: data.values },
message: `Edit table field values to "${JSON.stringify(
data.values,
)}"`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -92,7 +101,7 @@ export default function FieldDetails({ data, tid, index }) {
)}
{isSized(data.type) && (
<>
<div className="font-semibold">Size</div>
<div className="font-semibold">{t("size")}</div>
<InputNumber
className="my-2 w-full"
placeholder="Set length"
@@ -111,7 +120,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index,
undo: editField,
redo: { size: e.target.value },
message: `Edit table field size to ${e.target.value}`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -121,10 +133,10 @@ export default function FieldDetails({ data, tid, index }) {
)}
{hasPrecision(data.type) && (
<>
<div className="font-semibold">Precision</div>
<div className="font-semibold">{t("precision")}</div>
<Input
className="my-2 w-full"
placeholder="Set precision: size, d"
placeholder={t("set_precision")}
validateStatus={
!data.size || /^\d+,\s*\d+$|^$/.test(data.size)
? "default"
@@ -145,7 +157,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index,
undo: editField,
redo: { size: e.target.value },
message: `Edit table field precision to ${e.target.value}`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -155,10 +170,10 @@ export default function FieldDetails({ data, tid, index }) {
)}
{hasCheck(data.type) && (
<>
<div className="font-semibold">Check Expression</div>
<div className="font-semibold">{t("check")}</div>
<Input
className="mt-2"
placeholder="Set constraint"
placeholder={t("check")}
value={data.check}
disabled={data.increment}
onChange={(value) => updateField(tid, index, { check: value })}
@@ -175,19 +190,20 @@ export default function FieldDetails({ data, tid, index }) {
fid: index,
undo: editField,
redo: { check: e.target.value },
message: `Edit table field check expression to ${e.target.value}`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
}}
/>
<div className="text-xs mt-1">
*This will appear in the script as is.
</div>
<div className="text-xs mt-1">{t("this_will_appear_as_is")}</div>
</>
)}
<div className="flex justify-between items-center my-3">
<div className="font-medium">Unique</div>
<div className="font-medium">{t("unique")}</div>
<Checkbox
value="unique"
checked={data.unique}
@@ -216,7 +232,7 @@ export default function FieldDetails({ data, tid, index }) {
/>
</div>
<div className="flex justify-between items-center my-3">
<div className="font-medium">Autoincrement</div>
<div className="font-medium">{t("autoincrement")}</div>
<Checkbox
value="increment"
checked={data.increment}
@@ -242,9 +258,10 @@ export default function FieldDetails({ data, tid, index }) {
redo: {
[checkedValues.target.value]: checkedValues.target.checked,
},
message: `Edit table field to${
data.increment ? " not" : ""
} auto increment`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -255,10 +272,10 @@ export default function FieldDetails({ data, tid, index }) {
}}
/>
</div>
<div className="font-semibold">Comment</div>
<div className="font-semibold">{t("comment")}</div>
<TextArea
className="my-2"
placeholder="Add comment"
placeholder={t("comment")}
value={data.comment}
autosize
rows={2}
@@ -276,7 +293,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index,
undo: editField,
redo: { comment: e.target.value },
message: `Edit field comment to "${e.target.value}"`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -288,7 +308,7 @@ export default function FieldDetails({ data, tid, index }) {
block
onClick={() => deleteField(data, tid)}
>
Delete field
{t("delete")}
</Button>
</div>
);

View File

@@ -2,15 +2,17 @@ import { Action, ObjectType } from "../../../data/constants";
import { Input, Button, Popover, Checkbox, Select } from "@douyinfe/semi-ui";
import { IconMore, IconDeleteStroked } from "@douyinfe/semi-icons";
import { useTables, useUndoRedo } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function IndexDetails({ data, fields, iid, tid }) {
const { t } = useTranslation();
const { tables, updateTable } = useTables();
const { setUndoStack, setRedoStack } = useUndoRedo();
return (
<div className="flex justify-between items-center mb-2">
<Select
placeholder="Select fields"
placeholder={t("select_fields")}
multiple
validateStatus={data.fields.length === 0 ? "error" : "default"}
optionList={fields}
@@ -33,7 +35,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
fields: [...value],
name: `${value.join("_")}_index`,
},
message: `Edit index fields to "${JSON.stringify(value)}"`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[index field]",
}),
},
]);
setRedoStack([]);
@@ -45,7 +50,7 @@ export default function IndexDetails({ data, fields, iid, tid }) {
fields: [...value],
name: `${value.join("_")}_index`,
}
: index
: index,
),
});
}}
@@ -53,10 +58,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
<Popover
content={
<div className="px-1 popover-theme">
<div className="font-semibold mb-1">Index name: </div>
<Input value={data.name} placeholder="Index name" disabled />
<div className="font-semibold mb-1">{t("name")}: </div>
<Input value={data.name} placeholder={t("name")} disabled />
<div className="flex justify-between items-center my-3">
<div className="font-medium">Unique</div>
<div className="font-medium">{t("unique")}</div>
<Checkbox
value="unique"
checked={data.unique}
@@ -77,9 +82,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
[checkedValues.target.value]:
checkedValues.target.checked,
},
message: `Edit table field to${
data.unique ? " not" : ""
} unique`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[index field]",
}),
},
]);
setRedoStack([]);
@@ -91,7 +97,7 @@ export default function IndexDetails({ data, fields, iid, tid }) {
[checkedValues.target.value]:
checkedValues.target.checked,
}
: index
: index,
),
});
}}
@@ -110,7 +116,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
component: "index_delete",
tid: tid,
data: data,
message: `Delete index`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[delete index]",
}),
},
]);
setRedoStack([]);

View File

@@ -3,10 +3,12 @@ import { useSelect } from "../../../hooks";
import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons";
import { ObjectType } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function SearchBar({ tables }) {
const { setSelectedElement } = useSelect();
const [searchText, setSearchText] = useState("");
const { t } = useTranslation();
const filteredTable = useMemo(
() => tables.map((t) => t.name).filter((i) => i.includes(searchText)),
[tables, searchText],
@@ -18,8 +20,8 @@ export default function SearchBar({ tables }) {
value={searchText}
showClear
prefix={<IconSearch />}
placeholder="Search..."
emptyContent={<div className="p-3 popover-theme">No tables found</div>}
placeholder={t("search")}
emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
onChange={(v) => setSearchText(v)}
onSelect={(v) => {
const { id } = tables.find((t) => t.name === v);

View File

@@ -5,10 +5,13 @@ import { getSize, hasCheck, hasPrecision, isSized } from "../../../utils/toSQL";
import { useTables, useTypes, useUndoRedo } from "../../../hooks";
import { useState } from "react";
import FieldDetails from "./FieldDetails";
import { useTranslation } from "react-i18next";
export default function TableField({ data, tid, index }) {
const { updateField } = useTables();
const { types } = useTypes();
const { tables } = useTables();
const { t } = useTranslation();
const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({});
@@ -33,7 +36,10 @@ export default function TableField({ data, tid, index }) {
fid: index,
undo: editField,
redo: { name: e.target.value },
message: `Edit table field name to ${e.target.value}`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -69,7 +75,10 @@ export default function TableField({ data, tid, index }) {
fid: index,
undo: { type: data.type },
redo: { type: value },
message: `Edit table field type to ${value}`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -123,7 +132,7 @@ export default function TableField({ data, tid, index }) {
<Col span={3}>
<Button
type={data.notNull ? "primary" : "tertiary"}
title="Not Null"
title={t("not_null")}
theme={data.notNull ? "solid" : "light"}
onClick={() => {
setUndoStack((prev) => [
@@ -136,9 +145,10 @@ export default function TableField({ data, tid, index }) {
fid: index,
undo: { notNull: data.notNull },
redo: { notNull: !data.notNull },
message: `Edit table field to${
data.notNull ? "" : " not"
} null`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -151,7 +161,7 @@ export default function TableField({ data, tid, index }) {
<Col span={3}>
<Button
type={data.primary ? "primary" : "tertiary"}
title="Primary"
title={t("primary")}
theme={data.primary ? "solid" : "light"}
onClick={() => {
setUndoStack((prev) => [
@@ -164,9 +174,10 @@ export default function TableField({ data, tid, index }) {
fid: index,
undo: { primary: data.primary },
redo: { primary: !data.primary },
message: `Edit table field to${
data.primary ? " not" : ""
} primary`,
message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
},
]);
setRedoStack([]);

View File

@@ -1,23 +1,22 @@
import { useState } from "react";
import {
Collapse,
Row,
Col,
Input,
TextArea,
Button,
Card,
Popover,
Toast,
} from "@douyinfe/semi-ui";
import { IconDeleteStroked } from "@douyinfe/semi-icons";
import { useTables, useUndoRedo } from "../../../hooks";
import { Action, ObjectType, defaultBlue } from "../../../data/constants";
import ColorPalette from "../../ColorPalette";
import ColorPalette from "../../ColorPicker";
import TableField from "./TableField";
import IndexDetails from "./IndexDetails";
import { useTranslation } from "react-i18next";
export default function TableInfo({ data }) {
const { t } = useTranslation();
const [indexActiveKey, setIndexActiveKey] = useState("");
const { deleteTable, updateTable, updateField, setRelationships } =
useTables();
@@ -31,11 +30,11 @@ export default function TableInfo({ data }) {
return (
<div>
<div className="flex items-center mb-2.5">
<div className="text-md font-semibold">Name: </div>
<div className="text-md font-semibold break-keep">{t("name")}: </div>
<Input
value={data.name}
validateStatus={data.name === "" ? "error" : "default"}
placeholder="Name"
placeholder={t("name")}
className="ms-2"
onChange={(value) => updateTable(data.id, { name: value })}
onFocus={(e) => setEditField({ name: e.target.value })}
@@ -50,7 +49,10 @@ export default function TableInfo({ data }) {
tid: data.id,
undo: editField,
redo: { name: e.target.value },
message: `Edit table name to ${e.target.value}`,
message: t("edit_table", {
tableName: e.target.value,
extra: "[name]",
}),
},
]);
setRedoStack([]);
@@ -149,7 +151,7 @@ export default function TableInfo({ data }) {
onChange={(itemKey) => setIndexActiveKey(itemKey)}
accordion
>
<Collapse.Panel header="Indices" itemKey="1">
<Collapse.Panel header={t("indices")} itemKey="1">
{data.indices.map((idx, k) => (
<IndexDetails
key={"index_" + k}
@@ -172,12 +174,12 @@ export default function TableInfo({ data }) {
headerLine={false}
>
<Collapse keepDOM lazyRender>
<Collapse.Panel header="Comment" itemKey="1">
<Collapse.Panel header={t("comment")} itemKey="1">
<TextArea
field="comment"
value={data.comment}
autosize
placeholder="Add comment"
placeholder={t("comment")}
rows={1}
onChange={(value) =>
updateTable(data.id, { comment: value }, false)
@@ -194,7 +196,10 @@ export default function TableInfo({ data }) {
tid: data.id,
undo: editField,
redo: { comment: e.target.value },
message: `Edit table comment to ${e.target.value}`,
message: t("edit_table", {
tableName: e.target.value,
extra: "[comment]",
}),
},
]);
setRedoStack([]);
@@ -203,8 +208,8 @@ export default function TableInfo({ data }) {
</Collapse.Panel>
</Collapse>
</Card>
<Row gutter={6} className="mt-2">
<Col span={8}>
<div className="flex justify-between items-center gap-1 mb-2">
<div>
<Popover
content={
<div className="popover-theme">
@@ -220,7 +225,10 @@ export default function TableInfo({ data }) {
tid: data.id,
undo: { color: data.color },
redo: { color: defaultBlue },
message: `Edit table color to default`,
message: t("edit_table", {
tableName: data.name,
extra: "[color]",
}),
},
]);
setRedoStack([]);
@@ -236,7 +244,10 @@ export default function TableInfo({ data }) {
tid: data.id,
undo: { color: data.color },
redo: { color: c },
message: `Edit table color to ${c}`,
message: t("edit_table", {
tableName: data.name,
extra: "[color]",
}),
},
]);
setRedoStack([]);
@@ -250,12 +261,12 @@ export default function TableInfo({ data }) {
showArrow
>
<div
className="h-[32px] w-[32px] rounded mb-2"
className="h-[32px] w-[32px] rounded"
style={{ backgroundColor: data.color }}
/>
</Popover>
</Col>
<Col span={7}>
</div>
<div className="flex gap-1">
<Button
block
onClick={() => {
@@ -267,7 +278,10 @@ export default function TableInfo({ data }) {
element: ObjectType.TABLE,
component: "index_add",
tid: data.id,
message: `Add index`,
message: t("edit_table", {
tableName: data.name,
extra: "[add index]",
}),
},
]);
setRedoStack([]);
@@ -284,10 +298,8 @@ export default function TableInfo({ data }) {
});
}}
>
Add index
{t("add_index")}
</Button>
</Col>
<Col span={6}>
<Button
onClick={() => {
setUndoStack((prev) => [
@@ -297,7 +309,10 @@ export default function TableInfo({ data }) {
element: ObjectType.TABLE,
component: "field_add",
tid: data.id,
message: `Add field`,
message: t("edit_table", {
tableName: data.name,
extra: "[add field]",
}),
},
]);
setRedoStack([]);
@@ -321,20 +336,15 @@ export default function TableInfo({ data }) {
}}
block
>
Add field
{t("add_field")}
</Button>
</Col>
<Col span={3}>
<Button
icon={<IconDeleteStroked />}
type="danger"
onClick={() => {
Toast.success(`Table deleted!`);
deleteTable(data.id);
}}
onClick={() => deleteTable(data.id)}
/>
</Col>
</Row>
</div>
</div>
</div>
);
}

View File

@@ -1,29 +1,29 @@
import { Collapse, Row, Col, Button } from "@douyinfe/semi-ui";
import { Collapse, Button } from "@douyinfe/semi-ui";
import { IconPlus } from "@douyinfe/semi-icons";
import { useSelect, useTables } from "../../../hooks";
import { ObjectType } from "../../../data/constants";
import SearchBar from "./SearchBar";
import Empty from "../Empty";
import TableInfo from "./TableInfo";
import { useTranslation } from "react-i18next";
export default function TablesTab() {
const { tables, addTable } = useTables();
const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
return (
<>
<Row gutter={6}>
<Col span={16}>
<SearchBar tables={tables} />
</Col>
<Col span={8}>
<div className="flex gap-2">
<SearchBar tables={tables} />
<div>
<Button icon={<IconPlus />} block onClick={() => addTable()}>
Add table
{t("add_table")}
</Button>
</Col>
</Row>
</div>
</div>
{tables.length === 0 ? (
<Empty title="No tables" text="Start building your diagram!" />
<Empty title={t("no_tables")} text={t("no_tables_text")} />
) : (
<Collapse
activeKey={

View File

@@ -3,11 +3,13 @@ import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons";
import { useSelect, useTypes } from "../../../hooks";
import { ObjectType } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function Searchbar() {
const { types } = useTypes();
const [value, setValue] = useState("");
const { setSelectedElement } = useSelect();
const { t } = useTranslation();
const [filteredResult, setFilteredResult] = useState(
types.map((t) => t.name),
@@ -25,9 +27,9 @@ export default function Searchbar() {
value={value}
showClear
prefix={<IconSearch />}
placeholder="Search..."
placeholder={t("search")}
onSearch={(v) => handleStringSearch(v)}
emptyContent={<div className="p-3 popover-theme">No types found</div>}
emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
onChange={(v) => setValue(v)}
onSelect={(v) => {
const i = types.findIndex((t) => t.name === v);

View File

@@ -13,22 +13,25 @@ import {
import { IconDeleteStroked, IconMore } from "@douyinfe/semi-icons";
import { isSized, hasPrecision, getSize } from "../../../utils/toSQL";
import { useUndoRedo, useTypes } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function TypeField({ data, tid, fid }) {
const { types, updateType } = useTypes();
const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({});
const { t } = useTranslation();
return (
<Row gutter={6} className="hover-1 my-2">
<Col span={10}>
<Input
value={data.name}
validateStatus={data.name === "" ? "error" : "default"}
placeholder="Name"
placeholder={t("name")}
onChange={(value) =>
updateType(tid, {
fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, name: value } : e
id === fid ? { ...data, name: value } : e,
),
})
}
@@ -45,7 +48,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid,
undo: editField,
redo: { name: e.target.value },
message: `Edit type field name to ${e.target.value}`,
message: t("edit_type", {
typeName: data.name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -62,7 +68,7 @@ export default function TypeField({ data, tid, fid }) {
})),
...types
.filter(
(type) => type.name.toLowerCase() !== data.name.toLowerCase()
(type) => type.name.toLowerCase() !== data.name.toLowerCase(),
)
.map((type) => ({
label: type.name.toUpperCase(),
@@ -72,7 +78,7 @@ export default function TypeField({ data, tid, fid }) {
filter
value={data.type}
validateStatus={data.type === "" ? "error" : "default"}
placeholder="Type"
placeholder={t("type")}
onChange={(value) => {
if (value === data.type) return;
setUndoStack((prev) => [
@@ -85,7 +91,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid,
undo: { type: data?.type },
redo: { type: value },
message: `Edit type field type to ${value}`,
message: t("edit_type", {
typeName: data.name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -98,7 +107,7 @@ export default function TypeField({ data, tid, fid }) {
type: value,
values: data.values ? [...data.values] : [],
}
: e
: e,
),
});
} else if (isSized(value) || hasPrecision(value)) {
@@ -106,13 +115,13 @@ export default function TypeField({ data, tid, fid }) {
fields: types[tid].fields.map((e, id) =>
id === fid
? { ...data, type: value, size: getSize(value) }
: e
: e,
),
});
} else {
updateType(tid, {
fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, type: value } : e
id === fid ? { ...data, type: value } : e,
),
});
}
@@ -125,7 +134,9 @@ export default function TypeField({ data, tid, fid }) {
<div className="popover-theme w-[240px]">
{(data.type === "ENUM" || data.type === "SET") && (
<>
<div className="font-semibold mb-1">{data.type} values</div>
<div className="font-semibold mb-1">
{data.type} {t("values")}
</div>
<TagInput
separator={[",", ", ", " ,"]}
value={data.values}
@@ -135,11 +146,11 @@ export default function TypeField({ data, tid, fid }) {
: "default"
}
className="my-2"
placeholder="Use ',' for batch input"
placeholder={t("use_for_batch_input")}
onChange={(v) =>
updateType(tid, {
fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, values: v } : e
id === fid ? { ...data, values: v } : e,
),
})
}
@@ -160,9 +171,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid,
undo: editField,
redo: { values: data.values },
message: `Edit type field values to "${JSON.stringify(
data.values
)}"`,
message: t("edit_type", {
typeName: data.name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -172,15 +184,15 @@ export default function TypeField({ data, tid, fid }) {
)}
{isSized(data.type) && (
<>
<div className="font-semibold">Size</div>
<div className="font-semibold">{t("size")}</div>
<InputNumber
className="my-2 w-full"
placeholder="Set length"
placeholder={t("size")}
value={data.size}
onChange={(value) =>
updateType(tid, {
fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, size: value } : e
id === fid ? { ...data, size: value } : e,
),
})
}
@@ -197,7 +209,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid,
undo: editField,
redo: { size: e.target.value },
message: `Edit type field size to ${e.target.value}`,
message: t("edit_type", {
typeName: data.name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -207,10 +222,10 @@ export default function TypeField({ data, tid, fid }) {
)}
{hasPrecision(data.type) && (
<>
<div className="font-semibold">Precision</div>
<div className="font-semibold">{t("precision")}</div>
<Input
className="my-2 w-full"
placeholder="Set precision: (size, d)"
placeholder={t("set_precision")}
validateStatus={
/^\(\d+,\s*\d+\)$|^$/.test(data.size)
? "default"
@@ -220,7 +235,7 @@ export default function TypeField({ data, tid, fid }) {
onChange={(value) =>
updateType(tid, {
fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, size: value } : e
id === fid ? { ...data, size: value } : e,
),
})
}
@@ -237,7 +252,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid,
undo: editField,
redo: { size: e.target.value },
message: `Edit type field precision to ${e.target.value}`,
message: t("edit_type", {
typeName: data.name,
extra: "[field]",
}),
},
]);
setRedoStack([]);
@@ -259,7 +277,10 @@ export default function TypeField({ data, tid, fid }) {
tid: tid,
fid: fid,
data: data,
message: `Delete field`,
message: t("edit_type", {
typeName: data.name,
extra: "[delete field]",
}),
},
]);
updateType(tid, {
@@ -267,7 +288,7 @@ export default function TypeField({ data, tid, fid }) {
});
}}
>
Delete field
{t("delete")}
</Button>
</div>
}

View File

@@ -8,16 +8,17 @@ import {
TextArea,
Button,
Card,
Toast,
} from "@douyinfe/semi-ui";
import { IconDeleteStroked, IconPlus } from "@douyinfe/semi-icons";
import { useUndoRedo, useTypes } from "../../../hooks";
import TypeField from "./TypeField";
import { useTranslation } from "react-i18next";
export default function TypeInfo({ index, data }) {
const { deleteType, updateType } = useTypes();
const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({});
const { t } = useTranslation();
return (
<div id={`scroll_type_${index}`}>
@@ -30,11 +31,11 @@ export default function TypeInfo({ index, data }) {
itemKey={`${index}`}
>
<div className="flex items-center mb-2.5">
<div className="text-md font-semibold">Name: </div>
<div className="text-md font-semibold break-keep">{t("name")}: </div>
<Input
value={data.name}
validateStatus={data.name === "" ? "error" : "default"}
placeholder="Name"
placeholder={t("name")}
className="ms-2"
onChange={(value) => updateType(index, { name: value })}
onFocus={(e) => setEditField({ name: e.target.value })}
@@ -49,7 +50,10 @@ export default function TypeInfo({ index, data }) {
tid: index,
undo: editField,
redo: { name: e.target.value },
message: `Edit type name to ${e.target.value}`,
message: t("edit_type", {
typeName: data.name,
extra: "[name]",
}),
},
]);
setRedoStack([]);
@@ -65,12 +69,12 @@ export default function TypeInfo({ index, data }) {
headerLine={false}
>
<Collapse keepDOM lazyRender>
<Collapse.Panel header="Comment" itemKey="1">
<Collapse.Panel header={t("comment")} itemKey="1">
<TextArea
field="comment"
value={data.comment}
autosize
placeholder="Add comment"
placeholder={t("comment")}
rows={1}
onChange={(value) =>
updateType(index, { comment: value }, false)
@@ -87,7 +91,10 @@ export default function TypeInfo({ index, data }) {
tid: index,
undo: editField,
redo: { comment: e.target.value },
message: `Edit type comment to ${e.target.value}`,
message: t("edit_type", {
typeName: data.name,
extra: "[comment]",
}),
},
]);
setRedoStack([]);
@@ -108,7 +115,10 @@ export default function TypeInfo({ index, data }) {
element: ObjectType.TYPE,
component: "field_add",
tid: index,
message: `Add field to type`,
message: t("edit_type", {
typeName: data.name,
extra: "[add field]",
}),
},
]);
setRedoStack([]);
@@ -124,20 +134,17 @@ export default function TypeInfo({ index, data }) {
}}
block
>
Add field
{t("add_field")}
</Button>
</Col>
<Col span={12}>
<Button
icon={<IconDeleteStroked />}
type="danger"
onClick={() => {
Toast.success(`Type deleted!`);
deleteType(index);
}}
onClick={() => deleteType(index)}
block
>
Delete
{t("delete")}
</Button>
</Col>
</Row>

View File

@@ -1,58 +1,44 @@
import { Collapse, Row, Col, Button, Popover } from "@douyinfe/semi-ui";
import { Collapse, Button, Popover } from "@douyinfe/semi-ui";
import { IconPlus, IconInfoCircle } from "@douyinfe/semi-icons";
import { useSelect, useTypes } from "../../../hooks";
import { ObjectType } from "../../../data/constants";
import Searchbar from "./SearchBar";
import Empty from "../Empty";
import TypeInfo from "./TypeInfo";
import { useTranslation } from "react-i18next";
export default function TypesTab() {
const { types, addType } = useTypes();
const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
return (
<>
<Row gutter={6}>
<Col span={13}>
<Searchbar />
</Col>
<Col span={8}>
<div className="flex gap-2">
<Searchbar />
<div>
<Button icon={<IconPlus />} block onClick={() => addType()}>
Add type
{t("add_type")}
</Button>
</Col>
<Col span={3}>
<Popover
content={
<div className="w-[240px] text-sm space-y-2 popover-theme">
<div>
This feature is meant for object-relational DBMSs like{" "}
<strong>PostgreSQL</strong>.
</div>
<div>
If used for <strong>MySQL</strong> or <strong>MariaDB</strong>{" "}
a <code>JSON</code> type will be generated with the
corresponding json validation check.
</div>
<div>
If used for <strong>SQLite</strong> it will be translated to a{" "}
<code>BLOB</code>.
</div>
<div>
If used for <strong>MSSQL</strong> a type alias to the first
field will be generated.
</div>
</div>
}
showArrow
position="rightTop"
>
<Button theme="borderless" icon={<IconInfoCircle />} />
</Popover>
</Col>
</Row>
</div>
<Popover
content={
<div className="w-[240px] text-sm space-y-2 popover-theme">
{t("types_info")
.split("\n")
.map((line, index) => (
<div key={index}>{line}</div>
))}
</div>
}
showArrow
position="rightTop"
>
<Button theme="borderless" icon={<IconInfoCircle />} />
</Popover>
</div>
{types.length <= 0 ? (
<Empty title="No types" text="Make your own custom data types" />
<Empty title={t("no_types")} text={t("no_types_text")} />
) : (
<Collapse
activeKey={

View File

@@ -1,10 +1,12 @@
import { Divider, Tooltip } from "@douyinfe/semi-ui";
import { useTransform, useLayout } from "../hooks";
import { exitFullscreen } from "../utils/fullscreen";
import { useTranslation } from "react-i18next";
export default function FloatingControls() {
const { transform, setTransform } = useTransform();
const { setLayout } = useLayout();
const { t } = useTranslation();
return (
<div className="flex gap-2">
@@ -35,7 +37,7 @@ export default function FloatingControls() {
<i className="bi bi-plus-lg" />
</button>
</div>
<Tooltip content="Exit">
<Tooltip content={t("exit")}>
<button
className="px-3 py-2 rounded-lg popover-theme"
onClick={() => {