mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-04 22:13:15 +00:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60fe0843ac | ||
|
|
794f226209 | ||
|
|
2fbf3476b8 | ||
|
|
897ac60a82 | ||
|
|
18f228ca1d | ||
|
|
14de30b7aa | ||
|
|
3faa39e787 | ||
|
|
63b5ba0bb9 | ||
|
|
44eac7daff | ||
|
|
502472b083 | ||
|
|
52d2ea596c | ||
|
|
bd67ccfbcf | ||
|
|
62beb68fa1 | ||
|
|
09b1275475 | ||
|
|
5dd7fe75d1 | ||
|
|
2939320a15 | ||
|
|
a643852837 | ||
|
|
467ff697c9 | ||
|
|
d6919f3033 | ||
|
|
56382a9fdc | ||
|
|
e06eb2a48e | ||
|
|
543b716c77 | ||
|
|
b55d631146 | ||
|
|
ef118929ad | ||
|
|
68f48190c9 | ||
|
|
bba265ad43 | ||
|
|
cbc4e85a14 | ||
|
|
26a0a5b550 | ||
|
|
b935b7f251 | ||
|
|
a1c0cf102a | ||
|
|
ab89bad6d5 | ||
|
|
deb218423f | ||
|
|
48342471ac | ||
|
|
47bb87a88f | ||
|
|
a96c2e1078 | ||
|
|
26d95eed25 | ||
|
|
be65328f24 | ||
|
|
85fd14fa02 | ||
|
|
9c485b3b01 | ||
|
|
e993f1549c | ||
|
|
0db67ea42a | ||
|
|
b9e621bd68 | ||
|
|
93d59f8887 | ||
|
|
190e4f4ffa | ||
|
|
dc404c9d7e | ||
|
|
dd4324d64f | ||
|
|
1878083056 | ||
|
|
7b6271962a | ||
|
|
2edc8dfde8 | ||
|
|
004d530880 | ||
|
|
fd2cc9fcfc | ||
|
|
4c93326bb6 | ||
|
|
ef3d7a8b67 | ||
|
|
3b3be086b1 | ||
|
|
b424518212 | ||
|
|
99a8201398 | ||
|
|
eb9b41e4f6 | ||
|
|
fef6d3f499 | ||
|
|
14f11c27a7 | ||
|
|
2118bce0f0 | ||
|
|
88be6c1fd4 | ||
|
|
0dcc9b9568 | ||
|
|
ff3269ec05 | ||
|
|
659dc2e3e7 | ||
|
|
c36cd33180 | ||
|
|
58231c9139 | ||
|
|
1643e7bdeb | ||
|
|
42d4cbac8c | ||
|
|
7452ca6965 | ||
|
|
27aede7794 | ||
|
|
e9e2736cb2 |
113
CHANGELOG.md
113
CHANGELOG.md
@@ -1,5 +1,118 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.10.0](https://github.com/chartdb/chartdb/compare/v1.9.0...v1.10.0) (2025-03-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **cloudflare-d1:** add support to cloudflare-d1 + wrangler cli ([#632](https://github.com/chartdb/chartdb/issues/632)) ([794f226](https://github.com/chartdb/chartdb/commit/794f2262092fbe36e27e92220221ed98cb51ae37))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **dbml-editor:** dealing with dbml editor for non-generic db-type ([#624](https://github.com/chartdb/chartdb/issues/624)) ([14de30b](https://github.com/chartdb/chartdb/commit/14de30b7aaa0ccaca8372f0213b692266d53f0de))
|
||||||
|
* **export-sql:** move from AI sql-export for MySQL&MariaDB to deterministic script ([#628](https://github.com/chartdb/chartdb/issues/628)) ([2fbf347](https://github.com/chartdb/chartdb/commit/2fbf3476b87f1177af17de8242a74d195dae5f35))
|
||||||
|
* **export-sql:** move from AI sql-export for postgres to deterministic script ([#626](https://github.com/chartdb/chartdb/issues/626)) ([18f228c](https://github.com/chartdb/chartdb/commit/18f228ca1d5a6c6056cb7c3bfc24d04ec470edf1))
|
||||||
|
* **export-sql:** move from AI sql-export for sqlite to deterministic script ([#627](https://github.com/chartdb/chartdb/issues/627)) ([897ac60](https://github.com/chartdb/chartdb/commit/897ac60a829a00e9453d670cceeb2282e9e93f1c))
|
||||||
|
* **sidebar:** add sidebar for diagram objects ([#618](https://github.com/chartdb/chartdb/issues/618)) ([63b5ba0](https://github.com/chartdb/chartdb/commit/63b5ba0bb9934c4e5c5d0d1b6f995afbbd3acf36))
|
||||||
|
* **sidebar:** opens sidepanel in case its closed and click on sidebar ([#620](https://github.com/chartdb/chartdb/issues/620)) ([3faa39e](https://github.com/chartdb/chartdb/commit/3faa39e7875d836dfe526d94a10f8aed070ac1c1))
|
||||||
|
|
||||||
|
## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **canvas:** highlight the Show-All button when No-Tables are visible in the canvas ([#612](https://github.com/chartdb/chartdb/issues/612)) ([62beb68](https://github.com/chartdb/chartdb/commit/62beb68fa1ec22ccd4fe5e59a8ceb9d3e8f6d374))
|
||||||
|
* **chart max length:** add support for edit char max length ([#613](https://github.com/chartdb/chartdb/issues/613)) ([09b1275](https://github.com/chartdb/chartdb/commit/09b12754757b9625ca287d91a92cf0d83c9e2b89))
|
||||||
|
* **chart max length:** enable edit length from data type select box ([#616](https://github.com/chartdb/chartdb/issues/616)) ([bd67ccf](https://github.com/chartdb/chartdb/commit/bd67ccfbcf66b919453ca6c0bfd71e16772b3d8e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **cardinality:** set true as default ([#583](https://github.com/chartdb/chartdb/issues/583)) ([2939320](https://github.com/chartdb/chartdb/commit/2939320a15a9ccd9eccfe46c26e04ca1edca2420))
|
||||||
|
* **performance:** Optimize performance of field comments editing ([#610](https://github.com/chartdb/chartdb/issues/610)) ([5dd7fe7](https://github.com/chartdb/chartdb/commit/5dd7fe75d1b0378ba406c75183c5e2356730c3b4))
|
||||||
|
* remove Buckle dialog ([#617](https://github.com/chartdb/chartdb/issues/617)) ([502472b](https://github.com/chartdb/chartdb/commit/502472b08342be425e66e2b6c94e5fe37ba14aa9))
|
||||||
|
* **shorcuts:** add shortcut to toggle the theme ([#602](https://github.com/chartdb/chartdb/issues/602)) ([a643852](https://github.com/chartdb/chartdb/commit/a6438528375ab54d3ec7d80ac6b6ddd65ea8cf1e))
|
||||||
|
|
||||||
|
## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **add-docs:** add link to ChartDB documentation ([#597](https://github.com/chartdb/chartdb/issues/597)) ([b55d631](https://github.com/chartdb/chartdb/commit/b55d631146ff3a1f7d63c800d44b5d3d3a223c76))
|
||||||
|
* components config ([#591](https://github.com/chartdb/chartdb/issues/591)) ([cbc4e85](https://github.com/chartdb/chartdb/commit/cbc4e85a14e24a43f9ff470518f8fe2845046bdb))
|
||||||
|
* **docker config:** Environment Variable Handling and Configuration Logic ([#605](https://github.com/chartdb/chartdb/issues/605)) ([d6919f3](https://github.com/chartdb/chartdb/commit/d6919f30336cc846fe6e6505b5a5278aa14dcce6))
|
||||||
|
* **empty-state:** show diff buttons on import-dbml when triggered by empty ([#574](https://github.com/chartdb/chartdb/issues/574)) ([4834247](https://github.com/chartdb/chartdb/commit/48342471ac231922f2ca4455b74a9879127a54f1))
|
||||||
|
* **i18n:** add [FR] translation ([#579](https://github.com/chartdb/chartdb/issues/579)) ([ab89bad](https://github.com/chartdb/chartdb/commit/ab89bad6d544ba4c339a3360eeec7d29e5579511))
|
||||||
|
* **img-export:** add ChartDB watermark to exported image ([#588](https://github.com/chartdb/chartdb/issues/588)) ([b935b7f](https://github.com/chartdb/chartdb/commit/b935b7f25111d5f72b7f8d7c552a4ea5974f791e))
|
||||||
|
* **import-mssql:** fix import/export scripts to handle data correctly ([#598](https://github.com/chartdb/chartdb/issues/598)) ([e06eb2a](https://github.com/chartdb/chartdb/commit/e06eb2a48e6bd3bcf352f4bcf128214c7da4c1b1))
|
||||||
|
* **menu-backup:** update export to be backup ([#590](https://github.com/chartdb/chartdb/issues/590)) ([26a0a5b](https://github.com/chartdb/chartdb/commit/26a0a5b550ef5e47e89b00d0232dc98936f63f23))
|
||||||
|
* open create new diagram when there is no diagram ([#594](https://github.com/chartdb/chartdb/issues/594)) ([ef11892](https://github.com/chartdb/chartdb/commit/ef118929ad5d5cbfae0290061bd8ea30bd262496))
|
||||||
|
* **open diagram:** in case there is no diagram, opens the dialog ([#593](https://github.com/chartdb/chartdb/issues/593)) ([68f4819](https://github.com/chartdb/chartdb/commit/68f48190c93f155398cca15dd7af2a025de2d45f))
|
||||||
|
* **side-panel:** simplify how to add field and index ([#573](https://github.com/chartdb/chartdb/issues/573)) ([a1c0cf1](https://github.com/chartdb/chartdb/commit/a1c0cf102add4fb235e913e75078139b3961341b))
|
||||||
|
* **sql_server_export:** use sql server export ([#600](https://github.com/chartdb/chartdb/issues/600)) ([56382a9](https://github.com/chartdb/chartdb/commit/56382a9fdc5e3044f8811873dd8a79f590771896))
|
||||||
|
* **sqlite-import:** import nuallable columns correctly + add json type ([#571](https://github.com/chartdb/chartdb/issues/571)) ([deb2184](https://github.com/chartdb/chartdb/commit/deb218423f77f0c0945a93005696456f62b00ce3))
|
||||||
|
|
||||||
|
## [1.8.0](https://github.com/chartdb/chartdb/compare/v1.7.0...v1.8.0) (2025-02-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **dbml-import:** add error highlighting for dbml imports ([#556](https://github.com/chartdb/chartdb/issues/556)) ([190e4f4](https://github.com/chartdb/chartdb/commit/190e4f4ffa834fa621f264dc608ca3f3b393a331))
|
||||||
|
* **docker image:** add support for custom inference servers ([#543](https://github.com/chartdb/chartdb/issues/543)) ([1878083](https://github.com/chartdb/chartdb/commit/1878083056ea4db7a05cdeeb38a4f7b9f5f95bd1))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **canvas:** add right-click option to create relationships ([#568](https://github.com/chartdb/chartdb/issues/568)) ([e993f15](https://github.com/chartdb/chartdb/commit/e993f1549c4c86bb9e7e36062db803ba6613b3b3))
|
||||||
|
* **canvas:** locate table from canvas ([#560](https://github.com/chartdb/chartdb/issues/560)) ([dc404c9](https://github.com/chartdb/chartdb/commit/dc404c9d7ee272c93aac69646bac859829a5234e))
|
||||||
|
* **docker:** add option to hide popups ([#580](https://github.com/chartdb/chartdb/issues/580)) ([a96c2e1](https://github.com/chartdb/chartdb/commit/a96c2e107838d2dc13b586923fd9dbe06598cdd8))
|
||||||
|
* **export-sql:** show create script for only filtered schemas ([#570](https://github.com/chartdb/chartdb/issues/570)) ([85fd14f](https://github.com/chartdb/chartdb/commit/85fd14fa02bb2879c36bba53369dbf2e7fa578d4))
|
||||||
|
* **i18n:** fix Ukrainian ([#554](https://github.com/chartdb/chartdb/issues/554)) ([7b62719](https://github.com/chartdb/chartdb/commit/7b6271962a99bfe5ffbd0176e714c76368ef5c41))
|
||||||
|
* **import dbml:** add import for indexes ([#566](https://github.com/chartdb/chartdb/issues/566)) ([0db67ea](https://github.com/chartdb/chartdb/commit/0db67ea42a5f9585ca1d246db7a7ff0239bec0ba))
|
||||||
|
* **import-query:** improve the cleanup for messy json input ([#562](https://github.com/chartdb/chartdb/issues/562)) ([93d59f8](https://github.com/chartdb/chartdb/commit/93d59f8887765098d040a3184aaee32112f67267))
|
||||||
|
* **index unique:** extract unique toggle for faster editing ([#559](https://github.com/chartdb/chartdb/issues/559)) ([dd4324d](https://github.com/chartdb/chartdb/commit/dd4324d64f7638ada5c022a2ab38bd8e6986af25))
|
||||||
|
* **mssql-import:** improve script readability by adding edition comment ([#572](https://github.com/chartdb/chartdb/issues/572)) ([be65328](https://github.com/chartdb/chartdb/commit/be65328f24b0361638b9e2edb39eaa9906e77f67))
|
||||||
|
* **realtionships section:** add the schema to source/target tables ([#561](https://github.com/chartdb/chartdb/issues/561)) ([b9e621b](https://github.com/chartdb/chartdb/commit/b9e621bd680730a0ffbf1054d735bfa418711cae))
|
||||||
|
* **sqlserver-import:** open ssms guide when max chars ([#565](https://github.com/chartdb/chartdb/issues/565)) ([9c485b3](https://github.com/chartdb/chartdb/commit/9c485b3b01a131bf551c7e95916b0c416f6aa0b5))
|
||||||
|
* **table actions:** fix size of table actions ([#578](https://github.com/chartdb/chartdb/issues/578)) ([26d95ee](https://github.com/chartdb/chartdb/commit/26d95eed25d86452d9168a9d93a301ba50d934e3))
|
||||||
|
|
||||||
|
## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **dbml-editor:** add dbml editor in side pannel ([#534](https://github.com/chartdb/chartdb/issues/534)) ([88be6c1](https://github.com/chartdb/chartdb/commit/88be6c1fd4a7e1f20937e8204c14d8fc1c2665b4))
|
||||||
|
* **import-dbml:** add import dbml functionality ([#549](https://github.com/chartdb/chartdb/issues/549)) ([b424518](https://github.com/chartdb/chartdb/commit/b424518212290a870fdb7c420a303f65f5901429))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **canvas edit:** add option to edit names in canvas ([#536](https://github.com/chartdb/chartdb/issues/536)) ([0dcc9b9](https://github.com/chartdb/chartdb/commit/0dcc9b9568cfe749d44d2e93cb365ba3d3a1e71c))
|
||||||
|
* **dbml-editor:** add shortcuts to dbml and filter: [#534](https://github.com/chartdb/chartdb/issues/534) ([#535](https://github.com/chartdb/chartdb/issues/535)) ([3b3be08](https://github.com/chartdb/chartdb/commit/3b3be086b1e8d5acf999f8504580d9e2f956f7da))
|
||||||
|
* **dbml:** add error handling ([#545](https://github.com/chartdb/chartdb/issues/545)) ([fef6d3f](https://github.com/chartdb/chartdb/commit/fef6d3f4996130a3769d1f25b4b1f2090293a1bf))
|
||||||
|
* **empty-state:** fix dark-mode for empty-state ([#547](https://github.com/chartdb/chartdb/issues/547)) ([99a8201](https://github.com/chartdb/chartdb/commit/99a820139861546a012d7b562ddbb9b77698151a))
|
||||||
|
* **examples:** fix employee example dbml ([#544](https://github.com/chartdb/chartdb/issues/544)) ([2118bce](https://github.com/chartdb/chartdb/commit/2118bce0f00d55eb19d22b9fa2d4964ba2533a09))
|
||||||
|
* **i18n:** translation/Ukrainian ([#529](https://github.com/chartdb/chartdb/issues/529)) ([ff3269e](https://github.com/chartdb/chartdb/commit/ff3269ec0510bbae4bc114e65a1ea86a656e8785))
|
||||||
|
* **open-diagram:** add arrow keys navigation in open diagram dialog ([#537](https://github.com/chartdb/chartdb/issues/537)) ([14f11c2](https://github.com/chartdb/chartdb/commit/14f11c27a7ad5b990131c8495148cabf12835082))
|
||||||
|
* **performance:** fix bundle size ([#551](https://github.com/chartdb/chartdb/issues/551)) ([4c93326](https://github.com/chartdb/chartdb/commit/4c93326bb6e3eaa143373c500a0c641e95a53fb9))
|
||||||
|
* **performance:** reduce bundle size ([#553](https://github.com/chartdb/chartdb/issues/553)) ([004d530](https://github.com/chartdb/chartdb/commit/004d530880a50dea6e9786eb9ae63cf592a4d852))
|
||||||
|
* **performance:** resolve error on startup ([#552](https://github.com/chartdb/chartdb/issues/552)) ([fd2cc9f](https://github.com/chartdb/chartdb/commit/fd2cc9fcfc8f4a9f0bc79def47d89114159392fb))
|
||||||
|
* **psql-import:** remove typo for import command (psql) ([#546](https://github.com/chartdb/chartdb/issues/546)) ([eb9b41e](https://github.com/chartdb/chartdb/commit/eb9b41e4f656bec1451c45763f4ea5b547aeec5c))
|
||||||
|
* **scroll:** fix scroll area ([#550](https://github.com/chartdb/chartdb/issues/550)) ([ef3d7a8](https://github.com/chartdb/chartdb/commit/ef3d7a8b67431e923b75bf8287b86bbc8abe723b))
|
||||||
|
|
||||||
|
## [1.6.1](https://github.com/chartdb/chartdb/compare/v1.6.0...v1.6.1) (2025-01-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* change empty state image ([#531](https://github.com/chartdb/chartdb/issues/531)) ([42d4cba](https://github.com/chartdb/chartdb/commit/42d4cbac8ce352e0e4e155d7003bfb85296b897f))
|
||||||
|
* **chat-type:** remove typo of char datatype in examples ([#530](https://github.com/chartdb/chartdb/issues/530)) ([58231c9](https://github.com/chartdb/chartdb/commit/58231c91393de30ebff817f0ebc57a5c5579f106))
|
||||||
|
* **empty_state:** customize empty state ([#533](https://github.com/chartdb/chartdb/issues/533)) ([1643e7b](https://github.com/chartdb/chartdb/commit/1643e7bdeb1bbaf081ab064e871d102c87243c0a))
|
||||||
|
* **Image Export:** importing css rules error while download image ([#524](https://github.com/chartdb/chartdb/issues/524)) ([e9e2736](https://github.com/chartdb/chartdb/commit/e9e2736cb2203702d53df9afc30b8e989a8c9953))
|
||||||
|
* **shortcuts:** add zoom all shortcut ([#528](https://github.com/chartdb/chartdb/issues/528)) ([7452ca6](https://github.com/chartdb/chartdb/commit/7452ca6965b0332a93b686c397ddf51013e42506))
|
||||||
|
* **filter-tables:** show clean filter if no-results ([#532](https://github.com/chartdb/chartdb/issues/532)) ([c36cd33](https://github.com/chartdb/chartdb/commit/c36cd33180badaa9b7f9e27c765f19cb03a50ccd))
|
||||||
|
|
||||||
## [1.6.0](https://github.com/chartdb/chartdb/compare/v1.5.1...v1.6.0) (2025-01-02)
|
## [1.6.0](https://github.com/chartdb/chartdb/compare/v1.5.1...v1.6.0) (2025-01-02)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@@ -1,6 +1,9 @@
|
|||||||
FROM node:22-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
ARG VITE_OPENAI_API_KEY
|
ARG VITE_OPENAI_API_KEY
|
||||||
|
ARG VITE_OPENAI_API_ENDPOINT
|
||||||
|
ARG VITE_LLM_MODEL_NAME
|
||||||
|
ARG VITE_HIDE_BUCKLE_DOT_DEV
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
@@ -10,9 +13,13 @@ RUN npm ci
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
|
||||||
|
echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
|
||||||
|
echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \
|
||||||
|
echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Use a lightweight web server to serve the production build
|
|
||||||
FROM nginx:stable-alpine AS production
|
FROM nginx:stable-alpine AS production
|
||||||
|
|
||||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
||||||
@@ -20,7 +27,6 @@ COPY ./default.conf.template /etc/nginx/conf.d/default.conf.template
|
|||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# Expose the default port for the Nginx web server
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
61
README.md
61
README.md
@@ -49,13 +49,13 @@ Instantly visualize your database schema with a single **"Smart Query."** Custom
|
|||||||
|
|
||||||
**What it does**:
|
**What it does**:
|
||||||
|
|
||||||
- **Instant Schema Import**
|
- **Instant Schema Import**
|
||||||
Run a single query to instantly retrieve your database schema as JSON. This makes it incredibly fast to visualize your database schema, whether for documentation, team discussions, or simply understanding your data better.
|
Run a single query to instantly retrieve your database schema as JSON. This makes it incredibly fast to visualize your database schema, whether for documentation, team discussions, or simply understanding your data better.
|
||||||
|
|
||||||
- **AI-Powered Export for Easy Migration**
|
- **AI-Powered Export for Easy Migration**
|
||||||
Our AI-driven export feature allows you to generate the DDL script in the dialect of your choice. Whether you’re migrating from MySQL to PostgreSQL or from SQLite to MariaDB, ChartDB simplifies the process by providing the necessary scripts tailored to your target database.
|
Our AI-driven export feature allows you to generate the DDL script in the dialect of your choice. Whether you're migrating from MySQL to PostgreSQL or from SQLite to MariaDB, ChartDB simplifies the process by providing the necessary scripts tailored to your target database.
|
||||||
- **Interactive Editing**
|
- **Interactive Editing**
|
||||||
Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures.
|
Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures.
|
||||||
|
|
||||||
### Status
|
### Status
|
||||||
|
|
||||||
@@ -63,13 +63,13 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
|
|||||||
|
|
||||||
### Supported Databases
|
### Supported Databases
|
||||||
|
|
||||||
- ✅ PostgreSQL (<img src="./src/assets/postgresql_logo_2.png" width="15"/> + <img src="./src/assets/supabase.png" alt="Supabase" width="15"/> + <img src="./src/assets/timescale.png" alt="Timescale" width="15"/> )
|
- ✅ PostgreSQL (<img src="./src/assets/postgresql_logo_2.png" width="15"/> + <img src="./src/assets/supabase.png" alt="Supabase" width="15"/> + <img src="./src/assets/timescale.png" alt="Timescale" width="15"/> )
|
||||||
- ✅ MySQL
|
- ✅ MySQL
|
||||||
- ✅ SQL Server
|
- ✅ SQL Server
|
||||||
- ✅ MariaDB
|
- ✅ MariaDB
|
||||||
- ✅ SQLite
|
- ✅ SQLite (<img src="./src/assets/sqlite_logo_2.png" width="15"/> + <img src="./src/assets/cloudflare_d1.png" alt="Cloudflare D1" width="15"/> Cloudflare D1)
|
||||||
- ✅ CockroachDB
|
- ✅ CockroachDB
|
||||||
- ✅ ClickHouse
|
- ✅ ClickHouse
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -91,24 +91,51 @@ npm run build
|
|||||||
|
|
||||||
Or like this if you want to have AI capabilities:
|
Or like this if you want to have AI capabilities:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
npm install
|
npm install
|
||||||
VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
|
VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run the Docker Container
|
### Run the Docker Container
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest
|
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Build and Run locally
|
#### Build and Run locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t chartdb .
|
docker build -t chartdb .
|
||||||
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb
|
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Using Custom Inference Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
docker build \
|
||||||
|
--build-arg VITE_OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
|
||||||
|
--build-arg VITE_LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
|
||||||
|
-t chartdb .
|
||||||
|
|
||||||
|
# Run
|
||||||
|
docker run \
|
||||||
|
-e OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
|
||||||
|
-e LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
|
||||||
|
-p 8080:80 chartdb
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options.
|
||||||
|
|
||||||
Open your browser and navigate to `http://localhost:8080`.
|
Open your browser and navigate to `http://localhost:8080`.
|
||||||
|
|
||||||
|
Example configuration for a local vLLM server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_OPENAI_API_ENDPOINT=http://localhost:8000/v1
|
||||||
|
VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ
|
||||||
|
```
|
||||||
|
|
||||||
## Try it on our website
|
## Try it on our website
|
||||||
|
|
||||||
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
|
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
|
||||||
@@ -120,9 +147,9 @@ Open your browser and navigate to `http://localhost:8080`.
|
|||||||
|
|
||||||
## 💚 Community & Support
|
## 💚 Community & Support
|
||||||
|
|
||||||
- [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team)
|
- [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team)
|
||||||
- [GitHub Issues](https://github.com/chartdb/chartdb/issues) (For any bugs and errors you encounter using ChartDB)
|
- [GitHub Issues](https://github.com/chartdb/chartdb/issues) (For any bugs and errors you encounter using ChartDB)
|
||||||
- [Twitter](https://x.com/chartdb_io) (Get news fast)
|
- [Twitter](https://x.com/chartdb_io) (Get news fast)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "new-york",
|
||||||
"rsc": false,
|
"rsc": false,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "tailwind.config.js",
|
||||||
"css": "src/globals.css",
|
"css": "src/globals.css",
|
||||||
"baseColor": "slate",
|
"baseColor": "slate",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "src/components",
|
"components": "src/components",
|
||||||
"utils": "@/lib/utils"
|
"utils": "src/lib/utils",
|
||||||
}
|
"ui": "src/components/ui",
|
||||||
}
|
"lib": "src/lib",
|
||||||
|
"hooks": "src/hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ server {
|
|||||||
|
|
||||||
location /config.js {
|
location /config.js {
|
||||||
default_type application/javascript;
|
default_type application/javascript;
|
||||||
return 200 "window.env = { OPENAI_API_KEY: \"$OPENAI_API_KEY\" };";
|
return 200 "window.env = {
|
||||||
|
OPENAI_API_KEY: \"$OPENAI_API_KEY\",
|
||||||
|
OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
|
||||||
|
LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
|
||||||
|
HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
|
||||||
|
};";
|
||||||
}
|
}
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Replace placeholders in nginx.conf
|
# Replace placeholders in nginx.conf
|
||||||
envsubst '${OPENAI_API_KEY}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Start Nginx
|
# Start Nginx
|
||||||
nginx -g "daemon off;"
|
nginx -g "daemon off;"
|
||||||
|
|||||||
5097
package-lock.json
generated
5097
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.6.0",
|
"version": "1.10.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^0.0.51",
|
"@ai-sdk/openai": "^0.0.51",
|
||||||
|
"@dbml/core": "^3.9.5",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
@@ -21,27 +22,27 @@
|
|||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
"@radix-ui/react-collapsible": "^1.1.0",
|
"@radix-ui/react-collapsible": "^1.1.0",
|
||||||
"@radix-ui/react-context-menu": "^2.2.1",
|
"@radix-ui/react-context-menu": "^2.2.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-hover-card": "^1.1.1",
|
"@radix-ui/react-hover-card": "^1.1.1",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-menubar": "^1.1.1",
|
"@radix-ui/react-menubar": "^1.1.1",
|
||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "1.2.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"@xyflow/react": "^12.3.1",
|
"@xyflow/react": "^12.3.1",
|
||||||
"ahooks": "^3.8.1",
|
"ahooks": "^3.8.1",
|
||||||
"ai": "^3.3.14",
|
"ai": "^3.3.14",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
|
|||||||
BIN
src/assets/cloudflare_d1.png
Normal file
BIN
src/assets/cloudflare_d1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 937 B |
Binary file not shown.
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/empty_state_dark.png
Normal file
BIN
src/assets/empty_state_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
@@ -12,6 +12,14 @@ import { DarkTheme } from './themes/dark';
|
|||||||
import { LightTheme } from './themes/light';
|
import { LightTheme } from './themes/light';
|
||||||
import './config.ts';
|
import './config.ts';
|
||||||
|
|
||||||
|
export const Editor = lazy(() =>
|
||||||
|
import('./code-editor').then((module) => ({
|
||||||
|
default: module.Editor,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
type EditorType = typeof Editor;
|
||||||
|
|
||||||
export interface CodeSnippetProps {
|
export interface CodeSnippetProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -19,14 +27,9 @@ export interface CodeSnippetProps {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
autoScroll?: boolean;
|
autoScroll?: boolean;
|
||||||
isComplete?: boolean;
|
isComplete?: boolean;
|
||||||
|
editorProps?: React.ComponentProps<EditorType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = lazy(() =>
|
|
||||||
import('./code-editor').then((module) => ({
|
|
||||||
default: module.Editor,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||||
({
|
({
|
||||||
className,
|
className,
|
||||||
@@ -35,6 +38,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
language = 'sql',
|
language = 'sql',
|
||||||
autoScroll = false,
|
autoScroll = false,
|
||||||
isComplete = true,
|
isComplete = true,
|
||||||
|
editorProps,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const monaco = useMonaco();
|
const monaco = useMonaco();
|
||||||
@@ -144,27 +148,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
language={language}
|
language={language}
|
||||||
loading={<Spinner />}
|
loading={<Spinner />}
|
||||||
theme={effectiveTheme}
|
theme={effectiveTheme}
|
||||||
|
{...editorProps}
|
||||||
options={{
|
options={{
|
||||||
minimap: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
scrollbar: {
|
|
||||||
vertical: 'hidden',
|
|
||||||
horizontal: 'hidden',
|
|
||||||
alwaysConsumeMouseWheel: false,
|
|
||||||
},
|
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
renderValidationDecorations: 'off',
|
renderValidationDecorations: 'off',
|
||||||
lineDecorationsWidth: 0,
|
lineDecorationsWidth: 0,
|
||||||
overviewRulerBorder: false,
|
overviewRulerBorder: false,
|
||||||
overviewRulerLanes: 0,
|
overviewRulerLanes: 0,
|
||||||
hideCursorInOverviewRuler: true,
|
hideCursorInOverviewRuler: true,
|
||||||
|
contextmenu: false,
|
||||||
|
...editorProps?.options,
|
||||||
guides: {
|
guides: {
|
||||||
indentation: false,
|
indentation: false,
|
||||||
|
...editorProps?.options?.guides,
|
||||||
|
},
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'hidden',
|
||||||
|
horizontal: 'hidden',
|
||||||
|
alwaysConsumeMouseWheel: false,
|
||||||
|
...editorProps?.options?.scrollbar,
|
||||||
|
},
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
...editorProps?.options?.minimap,
|
||||||
},
|
},
|
||||||
contextmenu: false,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!isComplete ? (
|
{!isComplete ? (
|
||||||
|
|||||||
54
src/components/code-snippet/languages/dbml-language.ts
Normal file
54
src/components/code-snippet/languages/dbml-language.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Monaco } from '@monaco-editor/react';
|
||||||
|
import { dataTypes } from '@/lib/data/data-types/data-types';
|
||||||
|
|
||||||
|
export const setupDBMLLanguage = (monaco: Monaco) => {
|
||||||
|
monaco.languages.register({ id: 'dbml' });
|
||||||
|
|
||||||
|
// Define themes for DBML
|
||||||
|
monaco.editor.defineTheme('dbml-dark', {
|
||||||
|
base: 'vs-dark',
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords
|
||||||
|
{ token: 'string', foreground: 'CE9178' }, // Strings
|
||||||
|
{ token: 'annotation', foreground: '9CDCFE' }, // [annotations]
|
||||||
|
{ token: 'delimiter', foreground: 'D4D4D4' }, // Braces {}
|
||||||
|
{ token: 'operator', foreground: 'D4D4D4' }, // Operators
|
||||||
|
{ token: 'datatype', foreground: '4EC9B0' }, // Data types
|
||||||
|
],
|
||||||
|
colors: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
monaco.editor.defineTheme('dbml-light', {
|
||||||
|
base: 'vs',
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords
|
||||||
|
{ token: 'string', foreground: 'A31515' }, // Strings
|
||||||
|
{ token: 'annotation', foreground: '001080' }, // [annotations]
|
||||||
|
{ token: 'delimiter', foreground: '000000' }, // Braces {}
|
||||||
|
{ token: 'operator', foreground: '000000' }, // Operators
|
||||||
|
{ token: 'type', foreground: '267F99' }, // Data types
|
||||||
|
],
|
||||||
|
colors: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataTypesNames = dataTypes.map((dt) => dt.name);
|
||||||
|
const datatypePattern = dataTypesNames.join('|');
|
||||||
|
|
||||||
|
monaco.languages.setMonarchTokensProvider('dbml', {
|
||||||
|
keywords: ['Table', 'Ref', 'Indexes'],
|
||||||
|
datatypes: dataTypesNames,
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
|
||||||
|
[/\[.*?\]/, 'annotation'],
|
||||||
|
[/".*?"/, 'string'],
|
||||||
|
[/'.*?'/, 'string'],
|
||||||
|
[/[{}]/, 'delimiter'],
|
||||||
|
[/[<>]/, 'operator'],
|
||||||
|
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,30 +1,66 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import EmptyStateImage from '@/assets/empty_state.png';
|
import EmptyStateImage from '@/assets/empty_state.png';
|
||||||
|
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
|
||||||
import { Label } from '@/components/label/label';
|
import { Label } from '@/components/label/label';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
export interface EmptyStateProps {
|
export interface EmptyStateProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
imageClassName?: string;
|
||||||
|
titleClassName?: string;
|
||||||
|
descriptionClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmptyState = forwardRef<
|
export const EmptyState = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement> & EmptyStateProps
|
React.HTMLAttributes<HTMLDivElement> & EmptyStateProps
|
||||||
>(({ title, description, className }, ref) => (
|
>(
|
||||||
<div
|
(
|
||||||
ref={ref}
|
{
|
||||||
className={cn(
|
title,
|
||||||
'flex flex-1 flex-col items-center justify-center space-y-1',
|
description,
|
||||||
className
|
className,
|
||||||
)}
|
titleClassName,
|
||||||
>
|
descriptionClassName,
|
||||||
<img src={EmptyStateImage} alt="Empty state" className="w-32" />
|
imageClassName,
|
||||||
<Label className="text-base">{title}</Label>
|
},
|
||||||
<Label className="text-sm font-normal text-muted-foreground">
|
ref
|
||||||
{description}
|
) => {
|
||||||
</Label>
|
const { effectiveTheme } = useTheme();
|
||||||
</div>
|
|
||||||
));
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-1 flex-col items-center justify-center space-y-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
effectiveTheme === 'dark'
|
||||||
|
? EmptyStateImageDark
|
||||||
|
: EmptyStateImage
|
||||||
|
}
|
||||||
|
alt="Empty state"
|
||||||
|
className={cn('mb-2 w-20', imageClassName)}
|
||||||
|
/>
|
||||||
|
<Label className={cn('text-base', titleClassName)}>
|
||||||
|
{title}
|
||||||
|
</Label>
|
||||||
|
<Label
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-normal text-muted-foreground',
|
||||||
|
descriptionClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
EmptyState.displayName = 'EmptyState';
|
EmptyState.displayName = 'EmptyState';
|
||||||
|
|||||||
@@ -24,12 +24,19 @@ export interface SelectBoxOption {
|
|||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
regex?: string;
|
||||||
|
extractRegex?: RegExp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectBoxProps {
|
export interface SelectBoxProps {
|
||||||
options: SelectBoxOption[];
|
options: SelectBoxOption[];
|
||||||
value?: string[] | string;
|
value?: string[] | string;
|
||||||
onChange?: (values: string[] | string) => void;
|
valueSuffix?: string;
|
||||||
|
optionSuffix?: (option: SelectBoxOption) => string;
|
||||||
|
onChange?: (
|
||||||
|
values: string[] | string,
|
||||||
|
regexMatches?: string[] | string
|
||||||
|
) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string;
|
||||||
emptyPlaceholder?: string;
|
emptyPlaceholder?: string;
|
||||||
@@ -55,10 +62,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
className,
|
className,
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
|
valueSuffix,
|
||||||
onChange,
|
onChange,
|
||||||
multiple,
|
multiple,
|
||||||
oneLine,
|
oneLine,
|
||||||
selectAll,
|
selectAll,
|
||||||
|
optionSuffix,
|
||||||
deselectAll,
|
deselectAll,
|
||||||
clearText,
|
clearText,
|
||||||
showClear,
|
showClear,
|
||||||
@@ -86,7 +95,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSelect = React.useCallback(
|
const handleSelect = React.useCallback(
|
||||||
(selectedValue: string) => {
|
(selectedValue: string, regexMatches?: string[]) => {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
const newValue =
|
const newValue =
|
||||||
value?.includes(selectedValue) && Array.isArray(value)
|
value?.includes(selectedValue) && Array.isArray(value)
|
||||||
@@ -94,7 +103,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
: [...(value ?? []), selectedValue];
|
: [...(value ?? []), selectedValue];
|
||||||
onChange?.(newValue);
|
onChange?.(newValue);
|
||||||
} else {
|
} else {
|
||||||
onChange?.(selectedValue);
|
onChange?.(selectedValue, regexMatches);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -199,6 +208,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
(opt) => opt.value === value
|
(opt) => opt.value === value
|
||||||
)?.label
|
)?.label
|
||||||
}
|
}
|
||||||
|
{valueSuffix ? valueSuffix : ''}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -239,11 +249,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<Command
|
<Command
|
||||||
filter={(value, search) =>
|
filter={(value, search, keywords) => {
|
||||||
value.toLowerCase().includes(search.toLowerCase())
|
if (
|
||||||
|
keywords?.length &&
|
||||||
|
keywords.some((keyword) =>
|
||||||
|
new RegExp(keyword).test(search)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.toLowerCase())
|
||||||
? 1
|
? 1
|
||||||
: 0
|
: 0;
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CommandInput
|
<CommandInput
|
||||||
@@ -302,14 +323,36 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
const isSelected =
|
const isSelected =
|
||||||
Array.isArray(value) &&
|
Array.isArray(value) &&
|
||||||
value.includes(option.value);
|
value.includes(option.value);
|
||||||
|
|
||||||
|
const isRegexMatch =
|
||||||
|
option.regex &&
|
||||||
|
new RegExp(option.regex)?.test(
|
||||||
|
searchTerm
|
||||||
|
);
|
||||||
|
|
||||||
|
const matches = option.extractRegex
|
||||||
|
? searchTerm.match(
|
||||||
|
option.extractRegex
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
keywords={
|
||||||
|
option.regex
|
||||||
|
? [option.regex]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
// value={option.value}
|
// value={option.value}
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
handleSelect(
|
handleSelect(
|
||||||
option.value
|
option.value,
|
||||||
|
matches?.map(
|
||||||
|
(match) =>
|
||||||
|
match.toString()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -327,7 +370,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
)}
|
)}
|
||||||
<div className="flex items-center truncate">
|
<div className="flex items-center truncate">
|
||||||
<span>
|
<span>
|
||||||
{option.label}
|
{isRegexMatch
|
||||||
|
? searchTerm
|
||||||
|
: option.label}
|
||||||
|
{!isRegexMatch &&
|
||||||
|
optionSuffix
|
||||||
|
? optionSuffix(
|
||||||
|
option
|
||||||
|
)
|
||||||
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
{option.description && (
|
{option.description && (
|
||||||
<span className="ml-1 text-xs text-muted-foreground">
|
<span className="ml-1 text-xs text-muted-foreground">
|
||||||
@@ -337,19 +388,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!multiple &&
|
{((!multiple &&
|
||||||
option.value ===
|
option.value ===
|
||||||
value && (
|
value) ||
|
||||||
<CheckIcon
|
isRegexMatch) && (
|
||||||
className={cn(
|
<CheckIcon
|
||||||
'ml-auto',
|
className={cn(
|
||||||
option.value ===
|
'ml-auto',
|
||||||
value
|
option.value ===
|
||||||
? 'opacity-100'
|
value
|
||||||
: 'opacity-0'
|
? 'opacity-100'
|
||||||
)}
|
: 'opacity-0'
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
135
src/components/sheet/sheet.tsx
Normal file
135
src/components/sheet/sheet.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||||
|
|
||||||
|
const Sheet = SheetPrimitive.Root;
|
||||||
|
|
||||||
|
const SheetTrigger = SheetPrimitive.Trigger;
|
||||||
|
|
||||||
|
const SheetClose = SheetPrimitive.Close;
|
||||||
|
|
||||||
|
const SheetPortal = SheetPrimitive.Portal;
|
||||||
|
|
||||||
|
const SheetOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const sheetVariants = cva(
|
||||||
|
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
side: {
|
||||||
|
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||||
|
bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||||
|
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||||
|
right: 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
side: 'right',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SheetContentProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
|
VariantProps<typeof sheetVariants> {}
|
||||||
|
|
||||||
|
const SheetContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
|
SheetContentProps
|
||||||
|
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(sheetVariants({ side }), className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
|
<Cross2Icon className="size-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
{children}
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
));
|
||||||
|
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const SheetHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col space-y-2 text-center sm:text-left',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
SheetHeader.displayName = 'SheetHeader';
|
||||||
|
|
||||||
|
const SheetFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
SheetFooter.displayName = 'SheetFooter';
|
||||||
|
|
||||||
|
const SheetTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-lg font-semibold text-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const SheetDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetPortal,
|
||||||
|
SheetOverlay,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
};
|
||||||
790
src/components/sidebar/sidebar.tsx
Normal file
790
src/components/sidebar/sidebar.tsx
Normal file
@@ -0,0 +1,790 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
|
import { cva } from 'class-variance-authority';
|
||||||
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import { Separator } from '@/components/separator/separator';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from '@/components/sheet/sheet';
|
||||||
|
import { Skeleton } from '@/components/skeleton/skeleton';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/tooltip/tooltip';
|
||||||
|
import { ViewVerticalIcon } from '@radix-ui/react-icons';
|
||||||
|
import { useSidebar } from './use-sidebar';
|
||||||
|
|
||||||
|
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
|
||||||
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||||
|
const SIDEBAR_WIDTH = '16rem';
|
||||||
|
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||||
|
const SIDEBAR_WIDTH_ICON = '3rem';
|
||||||
|
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||||
|
|
||||||
|
type SidebarContext = {
|
||||||
|
state: 'expanded' | 'collapsed';
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
openMobile: boolean;
|
||||||
|
setOpenMobile: (open: boolean) => void;
|
||||||
|
isMobile: boolean;
|
||||||
|
toggleSidebar: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidebarContext = React.createContext<SidebarContext | null>(null);
|
||||||
|
|
||||||
|
const SidebarProvider = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'> & {
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
defaultOpen = true,
|
||||||
|
open: openProp,
|
||||||
|
onOpenChange: setOpenProp,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [openMobile, setOpenMobile] = React.useState(false);
|
||||||
|
|
||||||
|
// This is the internal state of the sidebar.
|
||||||
|
// We use openProp and setOpenProp for control from outside the component.
|
||||||
|
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||||
|
const open = openProp ?? _open;
|
||||||
|
const setOpen = React.useCallback(
|
||||||
|
(value: boolean | ((value: boolean) => boolean)) => {
|
||||||
|
const openState =
|
||||||
|
typeof value === 'function' ? value(open) : value;
|
||||||
|
if (setOpenProp) {
|
||||||
|
setOpenProp(openState);
|
||||||
|
} else {
|
||||||
|
_setOpen(openState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This sets the cookie to keep the sidebar state.
|
||||||
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||||
|
},
|
||||||
|
[setOpenProp, open]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper to toggle the sidebar.
|
||||||
|
const toggleSidebar = React.useCallback(() => {
|
||||||
|
return isMobile
|
||||||
|
? setOpenMobile((open) => !open)
|
||||||
|
: setOpen((open) => !open);
|
||||||
|
}, [isMobile, setOpen, setOpenMobile]);
|
||||||
|
|
||||||
|
// Adds a keyboard shortcut to toggle the sidebar.
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||||
|
(event.metaKey || event.ctrlKey)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [toggleSidebar]);
|
||||||
|
|
||||||
|
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||||
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||||
|
const state = open ? 'expanded' : 'collapsed';
|
||||||
|
|
||||||
|
const contextValue = React.useMemo<SidebarContext>(
|
||||||
|
() => ({
|
||||||
|
state,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
isMobile,
|
||||||
|
openMobile,
|
||||||
|
setOpenMobile,
|
||||||
|
toggleSidebar,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
state,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
isMobile,
|
||||||
|
openMobile,
|
||||||
|
setOpenMobile,
|
||||||
|
toggleSidebar,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarContext.Provider value={contextValue}>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--sidebar-width': SIDEBAR_WIDTH,
|
||||||
|
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||||
|
...style,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</SidebarContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
SidebarProvider.displayName = 'SidebarProvider';
|
||||||
|
|
||||||
|
const Sidebar = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'> & {
|
||||||
|
side?: 'left' | 'right';
|
||||||
|
variant?: 'sidebar' | 'floating' | 'inset';
|
||||||
|
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
side = 'left',
|
||||||
|
variant = 'sidebar',
|
||||||
|
collapsible = 'offcanvas',
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||||
|
|
||||||
|
if (collapsible === 'none') {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
open={openMobile}
|
||||||
|
onOpenChange={setOpenMobile}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SheetContent
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
data-mobile="true"
|
||||||
|
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
side={side}
|
||||||
|
>
|
||||||
|
<SheetHeader className="sr-only">
|
||||||
|
<SheetTitle>Sidebar</SheetTitle>
|
||||||
|
<SheetDescription>
|
||||||
|
Displays the mobile sidebar.
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<div className="flex size-full flex-col">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="group peer hidden text-sidebar-foreground md:block"
|
||||||
|
data-state={state}
|
||||||
|
data-collapsible={state === 'collapsed' ? collapsible : ''}
|
||||||
|
data-variant={variant}
|
||||||
|
data-side={side}
|
||||||
|
>
|
||||||
|
{/* This is what handles the sidebar gap on desktop */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
|
||||||
|
'group-data-[collapsible=offcanvas]:w-0',
|
||||||
|
'group-data-[side=right]:rotate-180',
|
||||||
|
variant === 'floating' || variant === 'inset'
|
||||||
|
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
|
||||||
|
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||||
|
side === 'left'
|
||||||
|
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||||
|
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||||
|
// Adjust the padding for floating and inset variants.
|
||||||
|
variant === 'floating' || variant === 'inset'
|
||||||
|
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
|
||||||
|
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Sidebar.displayName = 'Sidebar';
|
||||||
|
|
||||||
|
const SidebarTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Button>,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, onClick, ...props }, ref) => {
|
||||||
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="trigger"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn('h-7 w-7', className)}
|
||||||
|
onClick={(event) => {
|
||||||
|
onClick?.(event);
|
||||||
|
toggleSidebar();
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ViewVerticalIcon />
|
||||||
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarTrigger.displayName = 'SidebarTrigger';
|
||||||
|
|
||||||
|
const SidebarRail = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<'button'>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="rail"
|
||||||
|
aria-label="Toggle Sidebar"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
title="Toggle Sidebar"
|
||||||
|
className={cn(
|
||||||
|
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
||||||
|
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
|
||||||
|
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||||
|
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
|
||||||
|
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
||||||
|
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarRail.displayName = 'SidebarRail';
|
||||||
|
|
||||||
|
const SidebarInset = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'main'>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex w-full flex-1 flex-col bg-background',
|
||||||
|
'md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarInset.displayName = 'SidebarInset';
|
||||||
|
|
||||||
|
const SidebarInput = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Input>,
|
||||||
|
React.ComponentProps<typeof Input>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="input"
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarInput.displayName = 'SidebarInput';
|
||||||
|
|
||||||
|
const SidebarHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="header"
|
||||||
|
className={cn('flex flex-col gap-2 p-2', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarHeader.displayName = 'SidebarHeader';
|
||||||
|
|
||||||
|
const SidebarFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="footer"
|
||||||
|
className={cn('flex flex-col gap-2 p-2', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarFooter.displayName = 'SidebarFooter';
|
||||||
|
|
||||||
|
const SidebarSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Separator>,
|
||||||
|
React.ComponentProps<typeof Separator>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Separator
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="separator"
|
||||||
|
className={cn('mx-2 w-auto bg-sidebar-border', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarSeparator.displayName = 'SidebarSeparator';
|
||||||
|
|
||||||
|
const SidebarContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="content"
|
||||||
|
className={cn(
|
||||||
|
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarContent.displayName = 'SidebarContent';
|
||||||
|
|
||||||
|
const SidebarGroup = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="group"
|
||||||
|
className={cn(
|
||||||
|
'relative flex w-full min-w-0 flex-col p-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarGroup.displayName = 'SidebarGroup';
|
||||||
|
|
||||||
|
const SidebarGroupLabel = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'> & { asChild?: boolean }
|
||||||
|
>(({ className, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'div';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="group-label"
|
||||||
|
className={cn(
|
||||||
|
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarGroupLabel.displayName = 'SidebarGroupLabel';
|
||||||
|
|
||||||
|
const SidebarGroupAction = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<'button'> & { asChild?: boolean }
|
||||||
|
>(({ className, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="group-action"
|
||||||
|
className={cn(
|
||||||
|
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
'after:absolute after:-inset-2 after:md:hidden',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarGroupAction.displayName = 'SidebarGroupAction';
|
||||||
|
|
||||||
|
const SidebarGroupContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="group-content"
|
||||||
|
className={cn('w-full text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SidebarGroupContent.displayName = 'SidebarGroupContent';
|
||||||
|
|
||||||
|
const SidebarMenu = React.forwardRef<
|
||||||
|
HTMLUListElement,
|
||||||
|
React.ComponentProps<'ul'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ul
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu"
|
||||||
|
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SidebarMenu.displayName = 'SidebarMenu';
|
||||||
|
|
||||||
|
const SidebarMenuItem = React.forwardRef<
|
||||||
|
HTMLLIElement,
|
||||||
|
React.ComponentProps<'li'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<li
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-item"
|
||||||
|
className={cn('group/menu-item relative', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SidebarMenuItem.displayName = 'SidebarMenuItem';
|
||||||
|
|
||||||
|
const sidebarMenuButtonVariants = cva(
|
||||||
|
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||||
|
outline:
|
||||||
|
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-8 text-sm',
|
||||||
|
sm: 'h-7 text-xs',
|
||||||
|
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const SidebarMenuButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<'button'> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||||
|
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
asChild = false,
|
||||||
|
isActive = false,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'default',
|
||||||
|
tooltip,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
const { isMobile, state } = useSidebar();
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-button"
|
||||||
|
data-size={size}
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
sidebarMenuButtonVariants({ variant, size }),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tooltip) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof tooltip === 'string') {
|
||||||
|
tooltip = {
|
||||||
|
children: tooltip,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="right"
|
||||||
|
align="center"
|
||||||
|
hidden={state !== 'collapsed' || isMobile}
|
||||||
|
{...tooltip}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
SidebarMenuButton.displayName = 'SidebarMenuButton';
|
||||||
|
|
||||||
|
const SidebarMenuAction = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<'button'> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
showOnHover?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-action"
|
||||||
|
className={cn(
|
||||||
|
'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
'after:absolute after:-inset-2 after:md:hidden',
|
||||||
|
'peer-data-[size=sm]/menu-button:top-1',
|
||||||
|
'peer-data-[size=default]/menu-button:top-1.5',
|
||||||
|
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
showOnHover &&
|
||||||
|
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarMenuAction.displayName = 'SidebarMenuAction';
|
||||||
|
|
||||||
|
const SidebarMenuBadge = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground',
|
||||||
|
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
||||||
|
'peer-data-[size=sm]/menu-button:top-1',
|
||||||
|
'peer-data-[size=default]/menu-button:top-1.5',
|
||||||
|
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SidebarMenuBadge.displayName = 'SidebarMenuBadge';
|
||||||
|
|
||||||
|
const SidebarMenuSkeleton = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<'div'> & {
|
||||||
|
showIcon?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, showIcon = false, ...props }, ref) => {
|
||||||
|
// Random width between 50 to 90%.
|
||||||
|
const width = React.useMemo(() => {
|
||||||
|
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-skeleton"
|
||||||
|
className={cn(
|
||||||
|
'flex h-8 items-center gap-2 rounded-md px-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{showIcon && (
|
||||||
|
<Skeleton
|
||||||
|
className="size-4 rounded-md"
|
||||||
|
data-sidebar="menu-skeleton-icon"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Skeleton
|
||||||
|
className="h-4 max-w-[--skeleton-width] flex-1"
|
||||||
|
data-sidebar="menu-skeleton-text"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--skeleton-width': width,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
|
||||||
|
|
||||||
|
const SidebarMenuSub = React.forwardRef<
|
||||||
|
HTMLUListElement,
|
||||||
|
React.ComponentProps<'ul'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ul
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-sub"
|
||||||
|
className={cn(
|
||||||
|
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SidebarMenuSub.displayName = 'SidebarMenuSub';
|
||||||
|
|
||||||
|
const SidebarMenuSubItem = React.forwardRef<
|
||||||
|
HTMLLIElement,
|
||||||
|
React.ComponentProps<'li'>
|
||||||
|
>(({ ...props }, ref) => <li ref={ref} {...props} />);
|
||||||
|
SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
|
||||||
|
|
||||||
|
const SidebarMenuSubButton = React.forwardRef<
|
||||||
|
HTMLAnchorElement,
|
||||||
|
React.ComponentProps<'a'> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
>(({ asChild = false, size = 'md', isActive, className, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'a';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-sub-button"
|
||||||
|
data-size={size}
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
|
||||||
|
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||||
|
size === 'sm' && 'text-xs',
|
||||||
|
size === 'md' && 'text-sm',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupAction,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarInput,
|
||||||
|
SidebarInset,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuAction,
|
||||||
|
SidebarMenuBadge,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSkeleton,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
SidebarProvider,
|
||||||
|
SidebarRail,
|
||||||
|
SidebarSeparator,
|
||||||
|
SidebarTrigger,
|
||||||
|
SidebarContext,
|
||||||
|
};
|
||||||
11
src/components/sidebar/use-sidebar.tsx
Normal file
11
src/components/sidebar/use-sidebar.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SidebarContext } from './sidebar';
|
||||||
|
|
||||||
|
export const useSidebar = () => {
|
||||||
|
const context = React.useContext(SidebarContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useSidebar must be used within a SidebarProvider.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
16
src/components/skeleton/skeleton.tsx
Normal file
16
src/components/skeleton/skeleton.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function Skeleton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('animate-pulse rounded-md bg-primary/10', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton };
|
||||||
22
src/context/canvas-context/canvas-context.tsx
Normal file
22
src/context/canvas-context/canvas-context.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import { emptyFn } from '@/lib/utils';
|
||||||
|
import type { Graph } from '@/lib/graph';
|
||||||
|
import { createGraph } from '@/lib/graph';
|
||||||
|
|
||||||
|
export interface CanvasContext {
|
||||||
|
reorderTables: (options?: { updateHistory?: boolean }) => void;
|
||||||
|
fitView: (options?: {
|
||||||
|
duration?: number;
|
||||||
|
padding?: number;
|
||||||
|
maxZoom?: number;
|
||||||
|
}) => void;
|
||||||
|
setOverlapGraph: (graph: Graph<string>) => void;
|
||||||
|
overlapGraph: Graph<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canvasContext = createContext<CanvasContext>({
|
||||||
|
reorderTables: emptyFn,
|
||||||
|
fitView: emptyFn,
|
||||||
|
setOverlapGraph: emptyFn,
|
||||||
|
overlapGraph: createGraph(),
|
||||||
|
});
|
||||||
85
src/context/canvas-context/canvas-provider.tsx
Normal file
85
src/context/canvas-context/canvas-provider.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { type ReactNode, useCallback, useState } from 'react';
|
||||||
|
import { canvasContext } from './canvas-context';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import {
|
||||||
|
adjustTablePositions,
|
||||||
|
shouldShowTablesBySchemaFilter,
|
||||||
|
} from '@/lib/domain/db-table';
|
||||||
|
import { useReactFlow } from '@xyflow/react';
|
||||||
|
import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils';
|
||||||
|
import type { Graph } from '@/lib/graph';
|
||||||
|
import { createGraph } from '@/lib/graph';
|
||||||
|
|
||||||
|
interface CanvasProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||||
|
const { tables, relationships, updateTablesState, filteredSchemas } =
|
||||||
|
useChartDB();
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
|
const [overlapGraph, setOverlapGraph] =
|
||||||
|
useState<Graph<string>>(createGraph());
|
||||||
|
|
||||||
|
const reorderTables = useCallback(
|
||||||
|
(
|
||||||
|
options: { updateHistory?: boolean } = {
|
||||||
|
updateHistory: true,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const newTables = adjustTablePositions({
|
||||||
|
relationships,
|
||||||
|
tables: tables.filter((table) =>
|
||||||
|
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
||||||
|
),
|
||||||
|
mode: 'all', // Use 'all' mode for manual reordering
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedOverlapGraph = findOverlappingTables({
|
||||||
|
tables: newTables,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTablesState(
|
||||||
|
(currentTables) =>
|
||||||
|
currentTables.map((table) => {
|
||||||
|
const newTable = newTables.find(
|
||||||
|
(t) => t.id === table.id
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: table.id,
|
||||||
|
x: newTable?.x ?? table.x,
|
||||||
|
y: newTable?.y ?? table.y,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
updateHistory: options.updateHistory ?? true,
|
||||||
|
forceOverride: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setOverlapGraph(updatedOverlapGraph);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({
|
||||||
|
duration: 500,
|
||||||
|
padding: 0.2,
|
||||||
|
maxZoom: 0.8,
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
[filteredSchemas, relationships, tables, updateTablesState, fitView]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvasContext.Provider
|
||||||
|
value={{
|
||||||
|
reorderTables,
|
||||||
|
fitView,
|
||||||
|
setOverlapGraph,
|
||||||
|
overlapGraph,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</canvasContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -22,6 +22,8 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
|
|||||||
import { useEventEmitter } from 'ahooks';
|
import { useEventEmitter } from 'ahooks';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import { storageInitialValue } from '../storage-context/storage-context';
|
import { storageInitialValue } from '../storage-context/storage-context';
|
||||||
|
import { useDiff } from '../diff-context/use-diff';
|
||||||
|
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
||||||
|
|
||||||
export interface ChartDBProviderProps {
|
export interface ChartDBProviderProps {
|
||||||
diagram?: Diagram;
|
diagram?: Diagram;
|
||||||
@@ -30,7 +32,8 @@ export interface ChartDBProviderProps {
|
|||||||
|
|
||||||
export const ChartDBProvider: React.FC<
|
export const ChartDBProvider: React.FC<
|
||||||
React.PropsWithChildren<ChartDBProviderProps>
|
React.PropsWithChildren<ChartDBProviderProps>
|
||||||
> = ({ children, diagram, readonly }) => {
|
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||||
|
const { hasDiff } = useDiff();
|
||||||
let db = useStorage();
|
let db = useStorage();
|
||||||
const events = useEventEmitter<ChartDBEvent>();
|
const events = useEventEmitter<ChartDBEvent>();
|
||||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||||
@@ -53,9 +56,33 @@ export const ChartDBProvider: React.FC<
|
|||||||
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
||||||
diagram?.dependencies ?? []
|
diagram?.dependencies ?? []
|
||||||
);
|
);
|
||||||
|
const { events: diffEvents } = useDiff();
|
||||||
|
|
||||||
|
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
||||||
|
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
|
||||||
|
setTables((tables) =>
|
||||||
|
[...tables, ...(tablesAdded ?? [])].map((table) => {
|
||||||
|
const fields = fieldsAdded.get(table.id);
|
||||||
|
return fields
|
||||||
|
? { ...table, fields: [...table.fields, ...fields] }
|
||||||
|
: table;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setRelationships((relationships) => [
|
||||||
|
...relationships,
|
||||||
|
...(relationshipsAdded ?? []),
|
||||||
|
]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
diffEvents.useSubscription(diffCalculatedHandler);
|
||||||
|
|
||||||
const defaultSchemaName = defaultSchemas[databaseType];
|
const defaultSchemaName = defaultSchemas[databaseType];
|
||||||
|
|
||||||
|
const readonly = useMemo(
|
||||||
|
() => readonlyProp ?? hasDiff ?? false,
|
||||||
|
[readonlyProp, hasDiff]
|
||||||
|
);
|
||||||
|
|
||||||
if (readonly) {
|
if (readonly) {
|
||||||
db = storageInitialValue;
|
db = storageInitialValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sq
|
|||||||
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
|
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
|
||||||
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
||||||
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
||||||
|
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||||
|
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||||
|
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||||
|
|
||||||
export interface DialogContext {
|
export interface DialogContext {
|
||||||
// Create diagram dialog
|
// Create diagram dialog
|
||||||
@@ -13,7 +16,9 @@ export interface DialogContext {
|
|||||||
closeCreateDiagramDialog: () => void;
|
closeCreateDiagramDialog: () => void;
|
||||||
|
|
||||||
// Open diagram dialog
|
// Open diagram dialog
|
||||||
openOpenDiagramDialog: () => void;
|
openOpenDiagramDialog: (
|
||||||
|
params?: Omit<OpenDiagramDialogProps, 'dialog'>
|
||||||
|
) => void;
|
||||||
closeOpenDiagramDialog: () => void;
|
closeOpenDiagramDialog: () => void;
|
||||||
|
|
||||||
// Export SQL dialog
|
// Export SQL dialog
|
||||||
@@ -21,7 +26,9 @@ export interface DialogContext {
|
|||||||
closeExportSQLDialog: () => void;
|
closeExportSQLDialog: () => void;
|
||||||
|
|
||||||
// Create relationship dialog
|
// Create relationship dialog
|
||||||
openCreateRelationshipDialog: () => void;
|
openCreateRelationshipDialog: (
|
||||||
|
params?: Omit<CreateRelationshipDialogProps, 'dialog'>
|
||||||
|
) => void;
|
||||||
closeCreateRelationshipDialog: () => void;
|
closeCreateRelationshipDialog: () => void;
|
||||||
|
|
||||||
// Import database dialog
|
// Import database dialog
|
||||||
@@ -40,10 +47,6 @@ export interface DialogContext {
|
|||||||
openStarUsDialog: () => void;
|
openStarUsDialog: () => void;
|
||||||
closeStarUsDialog: () => void;
|
closeStarUsDialog: () => void;
|
||||||
|
|
||||||
// Buckle dialog
|
|
||||||
openBuckleDialog: () => void;
|
|
||||||
closeBuckleDialog: () => void;
|
|
||||||
|
|
||||||
// Export image dialog
|
// Export image dialog
|
||||||
openExportImageDialog: (
|
openExportImageDialog: (
|
||||||
params: Omit<ExportImageDialogProps, 'dialog'>
|
params: Omit<ExportImageDialogProps, 'dialog'>
|
||||||
@@ -61,6 +64,12 @@ export interface DialogContext {
|
|||||||
params: Omit<ImportDiagramDialogProps, 'dialog'>
|
params: Omit<ImportDiagramDialogProps, 'dialog'>
|
||||||
) => void;
|
) => void;
|
||||||
closeImportDiagramDialog: () => void;
|
closeImportDiagramDialog: () => void;
|
||||||
|
|
||||||
|
// Import DBML dialog
|
||||||
|
openImportDBMLDialog: (
|
||||||
|
params?: Omit<ImportDBMLDialogProps, 'dialog'>
|
||||||
|
) => void;
|
||||||
|
closeImportDBMLDialog: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dialogContext = createContext<DialogContext>({
|
export const dialogContext = createContext<DialogContext>({
|
||||||
@@ -84,6 +93,6 @@ export const dialogContext = createContext<DialogContext>({
|
|||||||
closeExportDiagramDialog: emptyFn,
|
closeExportDiagramDialog: emptyFn,
|
||||||
openImportDiagramDialog: emptyFn,
|
openImportDiagramDialog: emptyFn,
|
||||||
closeImportDiagramDialog: emptyFn,
|
closeImportDiagramDialog: emptyFn,
|
||||||
openBuckleDialog: emptyFn,
|
openImportDBMLDialog: emptyFn,
|
||||||
closeBuckleDialog: emptyFn,
|
closeImportDBMLDialog: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import React, { useCallback, useState } from 'react';
|
|||||||
import type { DialogContext } from './dialog-context';
|
import type { DialogContext } from './dialog-context';
|
||||||
import { dialogContext } from './dialog-context';
|
import { dialogContext } from './dialog-context';
|
||||||
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
|
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
|
||||||
|
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||||
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||||
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||||
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||||
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||||
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||||
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
|
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||||
@@ -17,18 +19,40 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
|
|||||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
||||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
||||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
||||||
import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog';
|
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||||
|
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||||
|
|
||||||
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
|
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
|
||||||
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
|
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
|
||||||
|
const [openDiagramDialogParams, setOpenDiagramDialogParams] =
|
||||||
|
useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
|
||||||
|
|
||||||
|
const openOpenDiagramDialogHandler: DialogContext['openOpenDiagramDialog'] =
|
||||||
|
useCallback(
|
||||||
|
(props) => {
|
||||||
|
setOpenDiagramDialogParams(props);
|
||||||
|
setOpenOpenDiagramDialog(true);
|
||||||
|
},
|
||||||
|
[setOpenOpenDiagramDialog]
|
||||||
|
);
|
||||||
|
|
||||||
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
|
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [createRelationshipDialogParams, setCreateRelationshipDialogParams] =
|
||||||
|
useState<Omit<CreateRelationshipDialogProps, 'dialog'>>();
|
||||||
|
const openCreateRelationshipDialogHandler: DialogContext['openCreateRelationshipDialog'] =
|
||||||
|
useCallback(
|
||||||
|
(params) => {
|
||||||
|
setCreateRelationshipDialogParams(params);
|
||||||
|
setOpenCreateRelationshipDialog(true);
|
||||||
|
},
|
||||||
|
[setOpenCreateRelationshipDialog]
|
||||||
|
);
|
||||||
|
|
||||||
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
||||||
const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
|
|
||||||
|
|
||||||
// Export image dialog
|
// Export image dialog
|
||||||
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
|
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
|
||||||
@@ -88,7 +112,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[setOpenTableSchemaDialog]
|
[setOpenTableSchemaDialog]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Export image dialog
|
// Export diagram dialog
|
||||||
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
|
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
@@ -96,17 +120,22 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
|
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
// Import DBML dialog
|
||||||
|
const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
|
||||||
|
const [importDBMLDialogParams, setImportDBMLDialogParams] =
|
||||||
|
useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialogContext.Provider
|
<dialogContext.Provider
|
||||||
value={{
|
value={{
|
||||||
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
|
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
|
||||||
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
|
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
|
||||||
openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true),
|
openOpenDiagramDialog: openOpenDiagramDialogHandler,
|
||||||
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
||||||
openExportSQLDialog: openExportSQLDialogHandler,
|
openExportSQLDialog: openExportSQLDialogHandler,
|
||||||
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
|
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
|
||||||
openCreateRelationshipDialog: () =>
|
openCreateRelationshipDialog:
|
||||||
setOpenCreateRelationshipDialog(true),
|
openCreateRelationshipDialogHandler,
|
||||||
closeCreateRelationshipDialog: () =>
|
closeCreateRelationshipDialog: () =>
|
||||||
setOpenCreateRelationshipDialog(false),
|
setOpenCreateRelationshipDialog(false),
|
||||||
openImportDatabaseDialog: openImportDatabaseDialogHandler,
|
openImportDatabaseDialog: openImportDatabaseDialogHandler,
|
||||||
@@ -116,8 +145,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
|
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
|
||||||
openStarUsDialog: () => setOpenStarUsDialog(true),
|
openStarUsDialog: () => setOpenStarUsDialog(true),
|
||||||
closeStarUsDialog: () => setOpenStarUsDialog(false),
|
closeStarUsDialog: () => setOpenStarUsDialog(false),
|
||||||
closeBuckleDialog: () => setOpenBuckleDialog(false),
|
|
||||||
openBuckleDialog: () => setOpenBuckleDialog(true),
|
|
||||||
closeExportImageDialog: () => setOpenExportImageDialog(false),
|
closeExportImageDialog: () => setOpenExportImageDialog(false),
|
||||||
openExportImageDialog: openExportImageDialogHandler,
|
openExportImageDialog: openExportImageDialogHandler,
|
||||||
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
|
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
|
||||||
@@ -126,17 +153,26 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
|
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
|
||||||
closeImportDiagramDialog: () =>
|
closeImportDiagramDialog: () =>
|
||||||
setOpenImportDiagramDialog(false),
|
setOpenImportDiagramDialog(false),
|
||||||
|
openImportDBMLDialog: (params) => {
|
||||||
|
setImportDBMLDialogParams(params);
|
||||||
|
setOpenImportDBMLDialog(true);
|
||||||
|
},
|
||||||
|
closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
|
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
|
||||||
<OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} />
|
<OpenDiagramDialog
|
||||||
|
dialog={{ open: openOpenDiagramDialog }}
|
||||||
|
{...openDiagramDialogParams}
|
||||||
|
/>
|
||||||
<ExportSQLDialog
|
<ExportSQLDialog
|
||||||
dialog={{ open: openExportSQLDialog }}
|
dialog={{ open: openExportSQLDialog }}
|
||||||
{...exportSQLDialogParams}
|
{...exportSQLDialogParams}
|
||||||
/>
|
/>
|
||||||
<CreateRelationshipDialog
|
<CreateRelationshipDialog
|
||||||
dialog={{ open: openCreateRelationshipDialog }}
|
dialog={{ open: openCreateRelationshipDialog }}
|
||||||
|
{...createRelationshipDialogParams}
|
||||||
/>
|
/>
|
||||||
<ImportDatabaseDialog
|
<ImportDatabaseDialog
|
||||||
dialog={{ open: openImportDatabaseDialog }}
|
dialog={{ open: openImportDatabaseDialog }}
|
||||||
@@ -153,7 +189,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
/>
|
/>
|
||||||
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
||||||
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
||||||
<BuckleDialog dialog={{ open: openBuckleDialog }} />
|
<ImportDBMLDialog
|
||||||
|
dialog={{ open: openImportDBMLDialog }}
|
||||||
|
{...importDBMLDialogParams}
|
||||||
|
/>
|
||||||
</dialogContext.Provider>
|
</dialogContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
433
src/context/diff-context/diff-check/diff-check.ts
Normal file
433
src/context/diff-context/diff-check/diff-check.ts
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type {
|
||||||
|
ChartDBDiff,
|
||||||
|
DiffMap,
|
||||||
|
DiffObject,
|
||||||
|
FieldDiffAttribute,
|
||||||
|
} from '../types';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DBIndex } from '@/lib/domain/db-index';
|
||||||
|
|
||||||
|
export function getDiffMapKey({
|
||||||
|
diffObject,
|
||||||
|
objectId,
|
||||||
|
attribute,
|
||||||
|
}: {
|
||||||
|
diffObject: DiffObject;
|
||||||
|
objectId: string;
|
||||||
|
attribute?: string;
|
||||||
|
}): string {
|
||||||
|
return attribute
|
||||||
|
? `${diffObject}-${attribute}-${objectId}`
|
||||||
|
: `${diffObject}-${objectId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDiff({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}): {
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
} {
|
||||||
|
const newDiffs = new Map<string, ChartDBDiff>();
|
||||||
|
const changedTables = new Map<string, boolean>();
|
||||||
|
const changedFields = new Map<string, boolean>();
|
||||||
|
|
||||||
|
// Compare tables
|
||||||
|
compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
|
||||||
|
|
||||||
|
// Compare fields and indexes for matching tables
|
||||||
|
compareTableContents({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap: newDiffs,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare relationships
|
||||||
|
compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
|
||||||
|
|
||||||
|
return { diffMap: newDiffs, changedTables, changedFields };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare tables between diagrams
|
||||||
|
function compareTables({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
const oldTables = diagram.tables || [];
|
||||||
|
const newTables = newDiagram.tables || [];
|
||||||
|
|
||||||
|
// Check for added tables
|
||||||
|
for (const newTable of newTables) {
|
||||||
|
if (!oldTables.find((t) => t.id === newTable.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'added',
|
||||||
|
tableId: newTable.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(newTable.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed tables
|
||||||
|
for (const oldTable of oldTables) {
|
||||||
|
if (!newTables.find((t) => t.id === oldTable.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'removed',
|
||||||
|
tableId: oldTable.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(oldTable.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for table name and comments changes
|
||||||
|
for (const oldTable of oldTables) {
|
||||||
|
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||||
|
|
||||||
|
if (!newTable) continue;
|
||||||
|
|
||||||
|
if (oldTable.name !== newTable.name) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: oldTable.id,
|
||||||
|
attribute: 'name',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'changed',
|
||||||
|
tableId: oldTable.id,
|
||||||
|
attributes: 'name',
|
||||||
|
newValue: newTable.name,
|
||||||
|
oldValue: oldTable.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
changedTables.set(oldTable.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldTable.comments !== newTable.comments) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: oldTable.id,
|
||||||
|
attribute: 'comments',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'changed',
|
||||||
|
tableId: oldTable.id,
|
||||||
|
attributes: 'comments',
|
||||||
|
newValue: newTable.comments,
|
||||||
|
oldValue: oldTable.comments,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
changedTables.set(oldTable.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare fields and indexes for matching tables
|
||||||
|
function compareTableContents({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
const oldTables = diagram.tables || [];
|
||||||
|
const newTables = newDiagram.tables || [];
|
||||||
|
|
||||||
|
// For each table that exists in both diagrams
|
||||||
|
for (const oldTable of oldTables) {
|
||||||
|
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||||
|
if (!newTable) continue;
|
||||||
|
|
||||||
|
// Compare fields
|
||||||
|
compareFields({
|
||||||
|
tableId: oldTable.id,
|
||||||
|
oldFields: oldTable.fields,
|
||||||
|
newFields: newTable.fields,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare indexes
|
||||||
|
compareIndexes({
|
||||||
|
tableId: oldTable.id,
|
||||||
|
oldIndexes: oldTable.indexes,
|
||||||
|
newIndexes: newTable.indexes,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare fields between tables
|
||||||
|
function compareFields({
|
||||||
|
tableId,
|
||||||
|
oldFields,
|
||||||
|
newFields,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
oldFields: DBField[];
|
||||||
|
newFields: DBField[];
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
// Check for added fields
|
||||||
|
for (const newField of newFields) {
|
||||||
|
if (!oldFields.find((f) => f.id === newField.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: newField.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'field',
|
||||||
|
type: 'added',
|
||||||
|
fieldId: newField.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
changedFields.set(newField.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed fields
|
||||||
|
for (const oldField of oldFields) {
|
||||||
|
if (!newFields.find((f) => f.id === oldField.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: oldField.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'field',
|
||||||
|
type: 'removed',
|
||||||
|
fieldId: oldField.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
changedFields.set(oldField.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for field changes
|
||||||
|
for (const oldField of oldFields) {
|
||||||
|
const newField = newFields.find((f) => f.id === oldField.id);
|
||||||
|
if (!newField) continue;
|
||||||
|
|
||||||
|
// Compare basic field properties
|
||||||
|
compareFieldProperties({
|
||||||
|
tableId,
|
||||||
|
oldField,
|
||||||
|
newField,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare field properties
|
||||||
|
function compareFieldProperties({
|
||||||
|
tableId,
|
||||||
|
oldField,
|
||||||
|
newField,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
oldField: DBField;
|
||||||
|
newField: DBField;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
const changedAttributes: FieldDiffAttribute[] = [];
|
||||||
|
|
||||||
|
if (oldField.name !== newField.name) {
|
||||||
|
changedAttributes.push('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.type.id !== newField.type.id) {
|
||||||
|
changedAttributes.push('type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.primaryKey !== newField.primaryKey) {
|
||||||
|
changedAttributes.push('primaryKey');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.unique !== newField.unique) {
|
||||||
|
changedAttributes.push('unique');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.nullable !== newField.nullable) {
|
||||||
|
changedAttributes.push('nullable');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.comments !== newField.comments) {
|
||||||
|
changedAttributes.push('comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedAttributes.length > 0) {
|
||||||
|
for (const attribute of changedAttributes) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: oldField.id,
|
||||||
|
attribute: attribute,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'field',
|
||||||
|
type: 'changed',
|
||||||
|
fieldId: oldField.id,
|
||||||
|
tableId,
|
||||||
|
attributes: attribute,
|
||||||
|
oldValue: oldField[attribute],
|
||||||
|
newValue: newField[attribute],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
changedFields.set(oldField.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare indexes between tables
|
||||||
|
function compareIndexes({
|
||||||
|
tableId,
|
||||||
|
oldIndexes,
|
||||||
|
newIndexes,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
oldIndexes: DBIndex[];
|
||||||
|
newIndexes: DBIndex[];
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
// Check for added indexes
|
||||||
|
for (const newIndex of newIndexes) {
|
||||||
|
if (!oldIndexes.find((i) => i.id === newIndex.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'index',
|
||||||
|
objectId: newIndex.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'index',
|
||||||
|
type: 'added',
|
||||||
|
indexId: newIndex.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed indexes
|
||||||
|
for (const oldIndex of oldIndexes) {
|
||||||
|
if (!newIndexes.find((i) => i.id === oldIndex.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'index',
|
||||||
|
objectId: oldIndex.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'index',
|
||||||
|
type: 'removed',
|
||||||
|
indexId: oldIndex.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare relationships between diagrams
|
||||||
|
function compareRelationships({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
}) {
|
||||||
|
const oldRelationships = diagram.relationships || [];
|
||||||
|
const newRelationships = newDiagram.relationships || [];
|
||||||
|
|
||||||
|
// Check for added relationships
|
||||||
|
for (const newRelationship of newRelationships) {
|
||||||
|
if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: newRelationship.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'relationship',
|
||||||
|
type: 'added',
|
||||||
|
relationshipId: newRelationship.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed relationships
|
||||||
|
for (const oldRelationship of oldRelationships) {
|
||||||
|
if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: oldRelationship.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'relationship',
|
||||||
|
type: 'removed',
|
||||||
|
relationshipId: oldRelationship.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/context/diff-context/diff-context.tsx
Normal file
75
src/context/diff-context/diff-context.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import type { DiffMap } from './types';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
export type DiffEventType = 'diff_calculated';
|
||||||
|
|
||||||
|
export type DiffEventBase<T extends DiffEventType, D> = {
|
||||||
|
action: T;
|
||||||
|
data: D;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiffCalculatedEvent = DiffEventBase<
|
||||||
|
'diff_calculated',
|
||||||
|
{
|
||||||
|
tablesAdded: DBTable[];
|
||||||
|
fieldsAdded: Map<string, DBField[]>;
|
||||||
|
relationshipsAdded: DBRelationship[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DiffEvent = DiffCalculatedEvent;
|
||||||
|
|
||||||
|
export interface DiffContext {
|
||||||
|
newDiagram: Diagram | null;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
hasDiff: boolean;
|
||||||
|
|
||||||
|
calculateDiff: ({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
// table diff
|
||||||
|
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
|
||||||
|
checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
|
||||||
|
checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
|
||||||
|
getTableNewName: ({ tableId }: { tableId: string }) => string | null;
|
||||||
|
|
||||||
|
// field diff
|
||||||
|
checkIfFieldHasChange: ({
|
||||||
|
tableId,
|
||||||
|
fieldId,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
fieldId: string;
|
||||||
|
}) => boolean;
|
||||||
|
checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean;
|
||||||
|
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
|
||||||
|
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
|
||||||
|
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
|
||||||
|
|
||||||
|
// relationship diff
|
||||||
|
checkIfNewRelationship: ({
|
||||||
|
relationshipId,
|
||||||
|
}: {
|
||||||
|
relationshipId: string;
|
||||||
|
}) => boolean;
|
||||||
|
checkIfRelationshipRemoved: ({
|
||||||
|
relationshipId,
|
||||||
|
}: {
|
||||||
|
relationshipId: string;
|
||||||
|
}) => boolean;
|
||||||
|
|
||||||
|
events: EventEmitter<DiffEvent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const diffContext = createContext<DiffContext | undefined>(undefined);
|
||||||
327
src/context/diff-context/diff-provider.tsx
Normal file
327
src/context/diff-context/diff-provider.tsx
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import type { DiffContext, DiffEvent } from './diff-context';
|
||||||
|
import { diffContext } from './diff-context';
|
||||||
|
import type { ChartDBDiff, DiffMap } from './types';
|
||||||
|
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import { useEventEmitter } from 'ahooks';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
|
||||||
|
const [diffMap, setDiffMap] = React.useState<DiffMap>(
|
||||||
|
new Map<string, ChartDBDiff>()
|
||||||
|
);
|
||||||
|
const [tablesChanged, setTablesChanged] = React.useState<
|
||||||
|
Map<string, boolean>
|
||||||
|
>(new Map<string, boolean>());
|
||||||
|
const [fieldsChanged, setFieldsChanged] = React.useState<
|
||||||
|
Map<string, boolean>
|
||||||
|
>(new Map<string, boolean>());
|
||||||
|
|
||||||
|
const events = useEventEmitter<DiffEvent>();
|
||||||
|
|
||||||
|
const generateNewFieldsMap = useCallback(
|
||||||
|
({
|
||||||
|
diffMap,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diffMap: DiffMap;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}) => {
|
||||||
|
const newFieldsMap = new Map<string, DBField[]>();
|
||||||
|
|
||||||
|
diffMap.forEach((diff) => {
|
||||||
|
if (diff.object === 'field' && diff.type === 'added') {
|
||||||
|
const field = newDiagram?.tables
|
||||||
|
?.find((table) => table.id === diff.tableId)
|
||||||
|
?.fields.find((f) => f.id === diff.fieldId);
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
newFieldsMap.set(diff.tableId, [
|
||||||
|
...(newFieldsMap.get(diff.tableId) ?? []),
|
||||||
|
field,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newFieldsMap;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const findNewRelationships = useCallback(
|
||||||
|
({
|
||||||
|
diffMap,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diffMap: DiffMap;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}) => {
|
||||||
|
const relationships: DBRelationship[] = [];
|
||||||
|
diffMap.forEach((diff) => {
|
||||||
|
if (diff.object === 'relationship' && diff.type === 'added') {
|
||||||
|
const relationship = newDiagram?.relationships?.find(
|
||||||
|
(rel) => rel.id === diff.relationshipId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relationship) {
|
||||||
|
relationships.push(relationship);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return relationships;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
|
||||||
|
({ diagram, newDiagram: newDiagramArg }) => {
|
||||||
|
const {
|
||||||
|
diffMap: newDiffs,
|
||||||
|
changedTables: newChangedTables,
|
||||||
|
changedFields: newChangedFields,
|
||||||
|
} = generateDiff({ diagram, newDiagram: newDiagramArg });
|
||||||
|
|
||||||
|
setDiffMap(newDiffs);
|
||||||
|
setTablesChanged(newChangedTables);
|
||||||
|
setFieldsChanged(newChangedFields);
|
||||||
|
setNewDiagram(newDiagramArg);
|
||||||
|
|
||||||
|
events.emit({
|
||||||
|
action: 'diff_calculated',
|
||||||
|
data: {
|
||||||
|
tablesAdded:
|
||||||
|
newDiagramArg?.tables?.filter((table) => {
|
||||||
|
const tableKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: table.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
newDiffs.has(tableKey) &&
|
||||||
|
newDiffs.get(tableKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
}) ?? [],
|
||||||
|
|
||||||
|
fieldsAdded: generateNewFieldsMap({
|
||||||
|
diffMap: newDiffs,
|
||||||
|
newDiagram: newDiagramArg,
|
||||||
|
}),
|
||||||
|
relationshipsAdded: findNewRelationships({
|
||||||
|
diffMap: newDiffs,
|
||||||
|
newDiagram: newDiagramArg,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setDiffMap, events, generateNewFieldsMap, findNewRelationships]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getTableNewName = useCallback<DiffContext['getTableNewName']>(
|
||||||
|
({ tableId }) => {
|
||||||
|
const tableNameKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: tableId,
|
||||||
|
attribute: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(tableNameKey)) {
|
||||||
|
const diff = diffMap.get(tableNameKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfTableHasChange = useCallback<
|
||||||
|
DiffContext['checkIfTableHasChange']
|
||||||
|
>(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]);
|
||||||
|
|
||||||
|
const checkIfNewTable = useCallback<DiffContext['checkIfNewTable']>(
|
||||||
|
({ tableId }) => {
|
||||||
|
const tableKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: tableId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfTableRemoved = useCallback<DiffContext['checkIfTableRemoved']>(
|
||||||
|
({ tableId }) => {
|
||||||
|
const tableKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: tableId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(tableKey) &&
|
||||||
|
diffMap.get(tableKey)?.type === 'removed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfFieldHasChange = useCallback<
|
||||||
|
DiffContext['checkIfFieldHasChange']
|
||||||
|
>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
return fieldsChanged.get(fieldId) ?? false;
|
||||||
|
},
|
||||||
|
[fieldsChanged]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfFieldRemoved = useCallback<DiffContext['checkIfFieldRemoved']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(fieldKey) &&
|
||||||
|
diffMap.get(fieldKey)?.type === 'removed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfNewField = useCallback<DiffContext['checkIfNewField']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewName = useCallback<DiffContext['getFieldNewName']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewType = useCallback<DiffContext['getFieldNewType']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'type',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as DataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfNewRelationship = useCallback<
|
||||||
|
DiffContext['checkIfNewRelationship']
|
||||||
|
>(
|
||||||
|
({ relationshipId }) => {
|
||||||
|
const relationshipKey = getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: relationshipId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(relationshipKey) &&
|
||||||
|
diffMap.get(relationshipKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfRelationshipRemoved = useCallback<
|
||||||
|
DiffContext['checkIfRelationshipRemoved']
|
||||||
|
>(
|
||||||
|
({ relationshipId }) => {
|
||||||
|
const relationshipKey = getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: relationshipId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(relationshipKey) &&
|
||||||
|
diffMap.get(relationshipKey)?.type === 'removed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<diffContext.Provider
|
||||||
|
value={{
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
hasDiff: diffMap.size > 0,
|
||||||
|
|
||||||
|
calculateDiff,
|
||||||
|
|
||||||
|
// table diff
|
||||||
|
getTableNewName,
|
||||||
|
checkIfNewTable,
|
||||||
|
checkIfTableRemoved,
|
||||||
|
checkIfTableHasChange,
|
||||||
|
|
||||||
|
// field diff
|
||||||
|
checkIfFieldHasChange,
|
||||||
|
checkIfFieldRemoved,
|
||||||
|
checkIfNewField,
|
||||||
|
getFieldNewName,
|
||||||
|
getFieldNewType,
|
||||||
|
|
||||||
|
// relationship diff
|
||||||
|
checkIfNewRelationship,
|
||||||
|
checkIfRelationshipRemoved,
|
||||||
|
|
||||||
|
events,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</diffContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
53
src/context/diff-context/types.ts
Normal file
53
src/context/diff-context/types.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
|
||||||
|
export type TableDiffAttribute = 'name' | 'comments';
|
||||||
|
|
||||||
|
export interface TableDiff {
|
||||||
|
object: 'table';
|
||||||
|
type: 'added' | 'removed' | 'changed';
|
||||||
|
tableId: string;
|
||||||
|
attributes?: TableDiffAttribute;
|
||||||
|
oldValue?: string;
|
||||||
|
newValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelationshipDiff {
|
||||||
|
object: 'relationship';
|
||||||
|
type: 'added' | 'removed';
|
||||||
|
relationshipId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldDiffAttribute =
|
||||||
|
| 'name'
|
||||||
|
| 'type'
|
||||||
|
| 'primaryKey'
|
||||||
|
| 'unique'
|
||||||
|
| 'nullable'
|
||||||
|
| 'comments';
|
||||||
|
|
||||||
|
export interface FieldDiff {
|
||||||
|
object: 'field';
|
||||||
|
type: 'added' | 'removed' | 'changed';
|
||||||
|
fieldId: string;
|
||||||
|
tableId: string;
|
||||||
|
attributes?: FieldDiffAttribute;
|
||||||
|
oldValue?: string | boolean | DataType;
|
||||||
|
newValue?: string | boolean | DataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IndexDiff {
|
||||||
|
object: 'index';
|
||||||
|
type: 'added' | 'removed';
|
||||||
|
indexId: string;
|
||||||
|
tableId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChartDBDiff = TableDiff | FieldDiff | IndexDiff | RelationshipDiff;
|
||||||
|
|
||||||
|
export type DiffMap = Map<string, ChartDBDiff>;
|
||||||
|
|
||||||
|
export type DiffObject =
|
||||||
|
| TableDiff['object']
|
||||||
|
| FieldDiff['object']
|
||||||
|
| IndexDiff['object']
|
||||||
|
| RelationshipDiff['object'];
|
||||||
10
src/context/diff-context/use-diff.ts
Normal file
10
src/context/diff-context/use-diff.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { diffContext } from './diff-context';
|
||||||
|
|
||||||
|
export const useDiff = () => {
|
||||||
|
const context = useContext(diffContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useDiff must be used within an DiffProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo, useEffect, useState } from 'react';
|
||||||
import type { ExportImageContext, ImageType } from './export-image-context';
|
import type { ExportImageContext, ImageType } from './export-image-context';
|
||||||
import { exportImageContext } from './export-image-context';
|
import { exportImageContext } from './export-image-context';
|
||||||
import { toJpeg, toPng, toSvg } from 'html-to-image';
|
import { toJpeg, toPng, toSvg } from 'html-to-image';
|
||||||
@@ -6,6 +6,8 @@ import { useReactFlow } from '@xyflow/react';
|
|||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
import logoDark from '@/assets/logo-dark.png';
|
||||||
|
import logoLight from '@/assets/logo-light.png';
|
||||||
|
|
||||||
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -14,6 +16,24 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const { setNodes, getViewport } = useReactFlow();
|
const { setNodes, getViewport } = useReactFlow();
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
const { diagramName } = useChartDB();
|
const { diagramName } = useChartDB();
|
||||||
|
const [logoBase64, setLogoBase64] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Convert logo to base64 on component mount
|
||||||
|
const img = new Image();
|
||||||
|
img.src = effectiveTheme === 'light' ? logoLight : logoDark;
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
const base64 = canvas.toDataURL('image/png');
|
||||||
|
setLogoBase64(base64);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [effectiveTheme]);
|
||||||
|
|
||||||
const downloadImage = useCallback(
|
const downloadImage = useCallback(
|
||||||
(dataUrl: string, type: ImageType) => {
|
(dataUrl: string, type: ImageType) => {
|
||||||
@@ -128,16 +148,22 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
'http://www.w3.org/2000/svg',
|
'http://www.w3.org/2000/svg',
|
||||||
'rect'
|
'rect'
|
||||||
);
|
);
|
||||||
const padding = 2000;
|
const bgPadding = 2000;
|
||||||
backgroundRect.setAttribute('x', String(-viewport.x - padding));
|
backgroundRect.setAttribute(
|
||||||
backgroundRect.setAttribute('y', String(-viewport.y - padding));
|
'x',
|
||||||
|
String(-viewport.x - bgPadding)
|
||||||
|
);
|
||||||
|
backgroundRect.setAttribute(
|
||||||
|
'y',
|
||||||
|
String(-viewport.y - bgPadding)
|
||||||
|
);
|
||||||
backgroundRect.setAttribute(
|
backgroundRect.setAttribute(
|
||||||
'width',
|
'width',
|
||||||
String(reactFlowBounds.width + 2 * padding)
|
String(reactFlowBounds.width + 2 * bgPadding)
|
||||||
);
|
);
|
||||||
backgroundRect.setAttribute(
|
backgroundRect.setAttribute(
|
||||||
'height',
|
'height',
|
||||||
String(reactFlowBounds.height + 2 * padding)
|
String(reactFlowBounds.height + 2 * bgPadding)
|
||||||
);
|
);
|
||||||
backgroundRect.setAttribute('fill', 'url(#background-pattern)');
|
backgroundRect.setAttribute('fill', 'url(#background-pattern)');
|
||||||
tempSvg.appendChild(backgroundRect);
|
tempSvg.appendChild(backgroundRect);
|
||||||
@@ -148,27 +174,110 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dataUrl = await imageCreateFn(viewportElement, {
|
// Handle SVG export differently
|
||||||
...(type === 'jpeg' || type === 'png'
|
if (type === 'svg') {
|
||||||
? {
|
const dataUrl = await imageCreateFn(viewportElement, {
|
||||||
backgroundColor:
|
width: reactFlowBounds.width,
|
||||||
effectiveTheme === 'light'
|
height: reactFlowBounds.height,
|
||||||
? '#ffffff'
|
style: {
|
||||||
: '#141414',
|
width: `${reactFlowBounds.width}px`,
|
||||||
}
|
height: `${reactFlowBounds.height}px`,
|
||||||
: {}),
|
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
||||||
width: reactFlowBounds.width,
|
},
|
||||||
height: reactFlowBounds.height,
|
quality: 1,
|
||||||
style: {
|
pixelRatio: scale,
|
||||||
width: `${reactFlowBounds.width}px`,
|
skipFonts: true,
|
||||||
height: `${reactFlowBounds.height}px`,
|
});
|
||||||
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
downloadImage(dataUrl, type);
|
||||||
},
|
return;
|
||||||
quality: 1,
|
}
|
||||||
pixelRatio: scale,
|
|
||||||
});
|
|
||||||
|
|
||||||
downloadImage(dataUrl, type);
|
// For PNG and JPEG, continue with the watermark process
|
||||||
|
const initialDataUrl = await imageCreateFn(
|
||||||
|
viewportElement,
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
effectiveTheme === 'light'
|
||||||
|
? '#ffffff'
|
||||||
|
: '#141414',
|
||||||
|
width: reactFlowBounds.width,
|
||||||
|
height: reactFlowBounds.height,
|
||||||
|
style: {
|
||||||
|
width: `${reactFlowBounds.width}px`,
|
||||||
|
height: `${reactFlowBounds.height}px`,
|
||||||
|
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
||||||
|
},
|
||||||
|
quality: 1,
|
||||||
|
pixelRatio: scale,
|
||||||
|
skipFonts: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a canvas to combine the diagram and watermark
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
downloadImage(initialDataUrl, type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set canvas size to match the export size
|
||||||
|
canvas.width = reactFlowBounds.width * scale;
|
||||||
|
canvas.height = reactFlowBounds.height * scale;
|
||||||
|
|
||||||
|
// Load the exported diagram
|
||||||
|
const diagramImage = new Image();
|
||||||
|
diagramImage.src = initialDataUrl;
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
diagramImage.onload = async () => {
|
||||||
|
// Draw the diagram
|
||||||
|
ctx.drawImage(diagramImage, 0, 0);
|
||||||
|
|
||||||
|
// Calculate logo size
|
||||||
|
const logoHeight = Math.max(
|
||||||
|
24,
|
||||||
|
Math.floor(canvas.width * 0.024)
|
||||||
|
);
|
||||||
|
const padding = Math.max(
|
||||||
|
12,
|
||||||
|
Math.floor(logoHeight * 0.5)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load and draw the logo
|
||||||
|
const logoImage = new Image();
|
||||||
|
logoImage.src = logoBase64;
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
logoImage.onload = () => {
|
||||||
|
// Calculate logo width while maintaining aspect ratio
|
||||||
|
const logoWidth =
|
||||||
|
(logoImage.width / logoImage.height) *
|
||||||
|
logoHeight;
|
||||||
|
|
||||||
|
// Draw logo in bottom-left corner
|
||||||
|
ctx.globalAlpha = 0.9;
|
||||||
|
ctx.drawImage(
|
||||||
|
logoImage,
|
||||||
|
padding,
|
||||||
|
canvas.height - logoHeight - padding,
|
||||||
|
logoWidth,
|
||||||
|
logoHeight
|
||||||
|
);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert canvas to data URL
|
||||||
|
const finalDataUrl = canvas.toDataURL(
|
||||||
|
type === 'png' ? 'image/png' : 'image/jpeg'
|
||||||
|
);
|
||||||
|
downloadImage(finalDataUrl, type);
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
viewportElement.removeChild(tempSvg);
|
viewportElement.removeChild(tempSvg);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -183,6 +292,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setNodes,
|
setNodes,
|
||||||
showLoader,
|
showLoader,
|
||||||
effectiveTheme,
|
effectiveTheme,
|
||||||
|
logoBase64,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useHistory } from '@/hooks/use-history';
|
|||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import { useReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -17,6 +18,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const { openOpenDiagramDialog } = useDialog();
|
const { openOpenDiagramDialog } = useDialog();
|
||||||
const { updateDiagramUpdatedAt } = useChartDB();
|
const { updateDiagramUpdatedAt } = useChartDB();
|
||||||
const { toggleSidePanel } = useLayout();
|
const { toggleSidePanel } = useLayout();
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination,
|
keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination,
|
||||||
@@ -37,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
|
keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
|
||||||
.keyCombination,
|
.keyCombination,
|
||||||
openOpenDiagramDialog,
|
() => openOpenDiagramDialog(),
|
||||||
{
|
{
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
@@ -61,6 +63,20 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
},
|
},
|
||||||
[toggleSidePanel]
|
[toggleSidePanel]
|
||||||
);
|
);
|
||||||
|
useHotkeys(
|
||||||
|
keyboardShortcutsForOS[KeyboardShortcutAction.SHOW_ALL].keyCombination,
|
||||||
|
() => {
|
||||||
|
fitView({
|
||||||
|
duration: 500,
|
||||||
|
padding: 0.1,
|
||||||
|
maxZoom: 0.8,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[fitView]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<keyboardShortcutsContext.Provider value={{}}>
|
<keyboardShortcutsContext.Provider value={{}}>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export enum KeyboardShortcutAction {
|
|||||||
OPEN_DIAGRAM = 'open_diagram',
|
OPEN_DIAGRAM = 'open_diagram',
|
||||||
SAVE_DIAGRAM = 'save_diagram',
|
SAVE_DIAGRAM = 'save_diagram',
|
||||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||||
|
SHOW_ALL = 'show_all',
|
||||||
|
TOGGLE_THEME = 'toggle_theme',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeyboardShortcut {
|
export interface KeyboardShortcut {
|
||||||
@@ -55,6 +57,20 @@ export const keyboardShortcuts: Record<
|
|||||||
keyCombinationMac: 'meta+b',
|
keyCombinationMac: 'meta+b',
|
||||||
keyCombinationWin: 'ctrl+b',
|
keyCombinationWin: 'ctrl+b',
|
||||||
},
|
},
|
||||||
|
[KeyboardShortcutAction.SHOW_ALL]: {
|
||||||
|
action: KeyboardShortcutAction.SHOW_ALL,
|
||||||
|
keyCombinationLabelMac: '⌘0',
|
||||||
|
keyCombinationLabelWin: 'Ctrl+0',
|
||||||
|
keyCombinationMac: 'meta+0',
|
||||||
|
keyCombinationWin: 'ctrl+0',
|
||||||
|
},
|
||||||
|
[KeyboardShortcutAction.TOGGLE_THEME]: {
|
||||||
|
action: KeyboardShortcutAction.TOGGLE_THEME,
|
||||||
|
keyCombinationLabelMac: '⌘M',
|
||||||
|
keyCombinationLabelWin: 'Ctrl+M',
|
||||||
|
keyCombinationMac: 'meta+m',
|
||||||
|
keyCombinationWin: 'ctrl+m',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface KeyboardShortcutForOS {
|
export interface KeyboardShortcutForOS {
|
||||||
|
|||||||
@@ -30,12 +30,6 @@ export interface LocalConfigContext {
|
|||||||
starUsDialogLastOpen: number;
|
starUsDialogLastOpen: number;
|
||||||
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
||||||
|
|
||||||
buckleWaitlistOpened: boolean;
|
|
||||||
setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void;
|
|
||||||
|
|
||||||
buckleDialogLastOpen: number;
|
|
||||||
setBuckleDialogLastOpen: (lastOpen: number) => void;
|
|
||||||
|
|
||||||
showDependenciesOnCanvas: boolean;
|
showDependenciesOnCanvas: boolean;
|
||||||
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
||||||
|
|
||||||
@@ -53,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
schemasFilter: {},
|
schemasFilter: {},
|
||||||
setSchemasFilter: emptyFn,
|
setSchemasFilter: emptyFn,
|
||||||
|
|
||||||
showCardinality: false,
|
showCardinality: true,
|
||||||
setShowCardinality: emptyFn,
|
setShowCardinality: emptyFn,
|
||||||
|
|
||||||
hideMultiSchemaNotification: false,
|
hideMultiSchemaNotification: false,
|
||||||
@@ -65,12 +59,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
starUsDialogLastOpen: 0,
|
starUsDialogLastOpen: 0,
|
||||||
setStarUsDialogLastOpen: emptyFn,
|
setStarUsDialogLastOpen: emptyFn,
|
||||||
|
|
||||||
buckleWaitlistOpened: false,
|
|
||||||
setBuckleWaitlistOpened: emptyFn,
|
|
||||||
|
|
||||||
buckleDialogLastOpen: 0,
|
|
||||||
setBuckleDialogLastOpen: emptyFn,
|
|
||||||
|
|
||||||
showDependenciesOnCanvas: false,
|
showDependenciesOnCanvas: false,
|
||||||
setShowDependenciesOnCanvas: emptyFn,
|
setShowDependenciesOnCanvas: emptyFn,
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ const showCardinalityKey = 'show_cardinality';
|
|||||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||||
const githubRepoOpenedKey = 'github_repo_opened';
|
const githubRepoOpenedKey = 'github_repo_opened';
|
||||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||||
const buckleWaitlistOpenedKey = 'buckle_waitlist_opened';
|
|
||||||
const buckleDialogLastOpenKey = 'buckle_dialog_last_open';
|
|
||||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
||||||
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
||||||
|
|
||||||
@@ -33,7 +31,7 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
||||||
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
|
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||||
@@ -51,17 +49,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
||||||
);
|
);
|
||||||
|
|
||||||
const [buckleWaitlistOpened, setBuckleWaitlistOpened] =
|
|
||||||
React.useState<boolean>(
|
|
||||||
(localStorage.getItem(buckleWaitlistOpenedKey) || 'false') ===
|
|
||||||
'true'
|
|
||||||
);
|
|
||||||
|
|
||||||
const [buckleDialogLastOpen, setBuckleDialogLastOpen] =
|
|
||||||
React.useState<number>(
|
|
||||||
parseInt(localStorage.getItem(buckleDialogLastOpenKey) || '0')
|
|
||||||
);
|
|
||||||
|
|
||||||
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
|
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
|
||||||
React.useState<boolean>(
|
React.useState<boolean>(
|
||||||
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
|
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
|
||||||
@@ -84,20 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
|
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
|
||||||
}, [githubRepoOpened]);
|
}, [githubRepoOpened]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
buckleDialogLastOpenKey,
|
|
||||||
buckleDialogLastOpen.toString()
|
|
||||||
);
|
|
||||||
}, [buckleDialogLastOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
buckleWaitlistOpenedKey,
|
|
||||||
buckleWaitlistOpened.toString()
|
|
||||||
);
|
|
||||||
}, [buckleWaitlistOpened]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
hideMultiSchemaNotificationKey,
|
hideMultiSchemaNotificationKey,
|
||||||
@@ -154,10 +127,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setStarUsDialogLastOpen,
|
setStarUsDialogLastOpen,
|
||||||
showDependenciesOnCanvas,
|
showDependenciesOnCanvas,
|
||||||
setShowDependenciesOnCanvas,
|
setShowDependenciesOnCanvas,
|
||||||
setBuckleDialogLastOpen,
|
|
||||||
buckleDialogLastOpen,
|
|
||||||
buckleWaitlistOpened,
|
|
||||||
setBuckleWaitlistOpened,
|
|
||||||
showMiniMapOnCanvas,
|
showMiniMapOnCanvas,
|
||||||
setShowMiniMapOnCanvas,
|
setShowMiniMapOnCanvas,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import type { EffectiveTheme } from './theme-context';
|
import type { EffectiveTheme } from './theme-context';
|
||||||
import { ThemeContext } from './theme-context';
|
import { ThemeContext } from './theme-context';
|
||||||
import { useMediaQuery } from 'react-responsive';
|
import { useMediaQuery } from 'react-responsive';
|
||||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import {
|
||||||
|
KeyboardShortcutAction,
|
||||||
|
keyboardShortcutsForOS,
|
||||||
|
} from '../keyboard-shortcuts-context/keyboard-shortcuts';
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
}
|
}
|
||||||
}, [effectiveTheme]);
|
}, [effectiveTheme]);
|
||||||
|
|
||||||
|
const handleThemeToggle = useCallback(() => {
|
||||||
|
if (theme === 'system') {
|
||||||
|
setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
|
||||||
|
} else {
|
||||||
|
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||||
|
}
|
||||||
|
}, [theme, effectiveTheme, setTheme]);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
keyboardShortcutsForOS[KeyboardShortcutAction.TOGGLE_THEME]
|
||||||
|
.keyCombination,
|
||||||
|
handleThemeToggle,
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[handleThemeToggle]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
|
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/dialog/dialog';
|
|
||||||
import { Button } from '@/components/button/button';
|
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
|
||||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
|
||||||
|
|
||||||
export interface BuckleDialogProps extends BaseDialogProps {}
|
|
||||||
|
|
||||||
export const BuckleDialog: React.FC<BuckleDialogProps> = ({ dialog }) => {
|
|
||||||
const { setBuckleWaitlistOpened } = useLocalConfig();
|
|
||||||
const { effectiveTheme } = useTheme();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!dialog.open) return;
|
|
||||||
}, [dialog.open]);
|
|
||||||
const { closeBuckleDialog } = useDialog();
|
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
|
||||||
setBuckleWaitlistOpened(true);
|
|
||||||
window.open('https://waitlist.buckle.dev', '_blank');
|
|
||||||
}, [setBuckleWaitlistOpened]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
{...dialog}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
closeBuckleDialog();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent
|
|
||||||
className="flex flex-col"
|
|
||||||
showClose={false}
|
|
||||||
onInteractOutside={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="hidden" />
|
|
||||||
<DialogDescription className="hidden" />
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex w-full flex-col items-center">
|
|
||||||
<img
|
|
||||||
src={
|
|
||||||
effectiveTheme === 'light'
|
|
||||||
? '/buckle-animated.gif'
|
|
||||||
: '/buckle.png'
|
|
||||||
}
|
|
||||||
className="h-16"
|
|
||||||
/>
|
|
||||||
<div className="mt-6 text-center text-base">
|
|
||||||
We've been working on something big -{' '}
|
|
||||||
<span className="font-semibold">Ready to explore?</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter className="flex gap-1 md:justify-between">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="secondary">Not now</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button onClick={handleConfirm}>
|
|
||||||
Try ChartDB v2.0!
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import {
|
import {
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@@ -31,6 +31,7 @@ import type { DatabaseClient } from '@/lib/domain/database-clients';
|
|||||||
import {
|
import {
|
||||||
databaseClientToLabelMap,
|
databaseClientToLabelMap,
|
||||||
databaseTypeToClientsMap,
|
databaseTypeToClientsMap,
|
||||||
|
databaseEditionToClientsMap,
|
||||||
} from '@/lib/domain/database-clients';
|
} from '@/lib/domain/database-clients';
|
||||||
import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
|
import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
|
||||||
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
|
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
|
||||||
@@ -71,7 +72,15 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
keepDialogAfterImport,
|
keepDialogAfterImport,
|
||||||
title,
|
title,
|
||||||
}) => {
|
}) => {
|
||||||
const databaseClients = databaseTypeToClientsMap[databaseType];
|
const databaseClients = useMemo(
|
||||||
|
() => [
|
||||||
|
...databaseTypeToClientsMap[databaseType],
|
||||||
|
...(databaseEdition
|
||||||
|
? databaseEditionToClientsMap[databaseEdition]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
[databaseType, databaseEdition]
|
||||||
|
);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [databaseClient, setDatabaseClient] = useState<
|
const [databaseClient, setDatabaseClient] = useState<
|
||||||
DatabaseClient | undefined
|
DatabaseClient | undefined
|
||||||
@@ -85,6 +94,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
|
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
|
||||||
const [isCheckingJson, setIsCheckingJson] = useState(false);
|
const [isCheckingJson, setIsCheckingJson] = useState(false);
|
||||||
|
|
||||||
|
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadScripts = async () => {
|
const loadScripts = async () => {
|
||||||
const { importMetadataScripts } = await import(
|
const { importMetadataScripts } = await import(
|
||||||
@@ -127,6 +138,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const inputValue = e.target.value;
|
const inputValue = e.target.value;
|
||||||
setScriptResult(inputValue);
|
setScriptResult(inputValue);
|
||||||
|
|
||||||
|
// Automatically open SSMS info when input length is exactly 65535
|
||||||
|
if (inputValue.length === 65535) {
|
||||||
|
setShowSSMSInfoDialog(true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[setScriptResult]
|
[setScriptResult]
|
||||||
);
|
);
|
||||||
@@ -245,10 +261,13 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
{t('new_diagram_dialog.import_database.step_1')}
|
{t('new_diagram_dialog.import_database.step_1')}
|
||||||
</div>
|
</div>
|
||||||
{databaseType === DatabaseType.SQL_SERVER && (
|
{databaseType === DatabaseType.SQL_SERVER && (
|
||||||
<SSMSInfo />
|
<SSMSInfo
|
||||||
|
open={showSSMSInfoDialog}
|
||||||
|
setOpen={setShowSSMSInfoDialog}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{databaseTypeToClientsMap[databaseType].length > 0 ? (
|
{databaseClients.length > 0 ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
value={
|
value={
|
||||||
!databaseClient
|
!databaseClient
|
||||||
@@ -369,6 +388,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
showCheckJsonButton,
|
showCheckJsonButton,
|
||||||
isCheckingJson,
|
isCheckingJson,
|
||||||
handleCheckJson,
|
handleCheckJson,
|
||||||
|
showSSMSInfoDialog,
|
||||||
|
setShowSSMSInfoDialog,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const renderFooter = useCallback(() => {
|
const renderFooter = useCallback(() => {
|
||||||
|
|||||||
@@ -4,32 +4,55 @@ import {
|
|||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from '@/components/hover-card/hover-card';
|
} from '@/components/hover-card/hover-card';
|
||||||
import { Label } from '@/components/label/label';
|
import { Label } from '@/components/label/label';
|
||||||
import { Info } from 'lucide-react';
|
import { Info, X } from 'lucide-react';
|
||||||
import React from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import SSMSInstructions from '@/assets/ssms-instructions.png';
|
import SSMSInstructions from '@/assets/ssms-instructions.png';
|
||||||
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
|
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface SSMSInfoProps {}
|
export interface SSMSInfoProps {
|
||||||
|
open?: boolean;
|
||||||
|
setOpen?: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const SSMSInfo = React.forwardRef<
|
export const SSMSInfo = React.forwardRef<
|
||||||
React.ElementRef<typeof HoverCardTrigger>,
|
React.ElementRef<typeof HoverCardTrigger>,
|
||||||
SSMSInfoProps
|
SSMSInfoProps
|
||||||
>((props, ref) => {
|
>(({ open: controlledOpen, setOpen: setControlledOpen }, ref) => {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (controlledOpen) {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}, [controlledOpen]);
|
||||||
|
|
||||||
|
const closeHandler = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
setControlledOpen?.(false);
|
||||||
|
}, [setControlledOpen]);
|
||||||
|
|
||||||
|
const isOpen = useMemo(
|
||||||
|
() => open || controlledOpen,
|
||||||
|
[open, controlledOpen]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCard
|
<HoverCard
|
||||||
open={open}
|
open={isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
|
if (controlledOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setOpen(isOpen);
|
setOpen(isOpen);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HoverCardTrigger ref={ref} {...props} asChild>
|
<HoverCardTrigger ref={ref} asChild>
|
||||||
<div
|
<div
|
||||||
className="flex flex-row items-center gap-1 text-pink-600"
|
className="flex flex-row items-center gap-1 text-pink-600"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpen(!open);
|
setOpen?.(!open);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Info size={14} />
|
<Info size={14} />
|
||||||
@@ -41,13 +64,21 @@ export const SSMSInfo = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="w-80">
|
<HoverCardContent className="w-80">
|
||||||
<div className="flex">
|
<div className="flex flex-col">
|
||||||
<div className="space-y-1">
|
<div className="flex items-start justify-between">
|
||||||
<h4 className="text-sm font-semibold">
|
<h4 className="text-sm font-semibold">
|
||||||
{t(
|
{t(
|
||||||
'new_diagram_dialog.import_database.ssms_instructions.title'
|
'new_diagram_dialog.import_database.ssms_instructions.title'
|
||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
|
<button
|
||||||
|
onClick={closeHandler}
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
<span className="font-semibold">1. </span>
|
<span className="font-semibold">1. </span>
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
||||||
DatabaseType.GENERIC
|
DatabaseType.GENERIC
|
||||||
);
|
);
|
||||||
const { closeCreateDiagramDialog } = useDialog();
|
const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
|
||||||
const { updateConfig } = useConfig();
|
const { updateConfig } = useConfig();
|
||||||
const [scriptResult, setScriptResult] = useState('');
|
const [scriptResult, setScriptResult] = useState('');
|
||||||
const [databaseEdition, setDatabaseEdition] = useState<
|
const [databaseEdition, setDatabaseEdition] = useState<
|
||||||
@@ -41,6 +41,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
const [diagramNumber, setDiagramNumber] = useState<number>(1);
|
const [diagramNumber, setDiagramNumber] = useState<number>(1);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDatabaseEdition(undefined);
|
||||||
|
}, [databaseType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDiagrams = async () => {
|
const fetchDiagrams = async () => {
|
||||||
const diagrams = await listDiagrams();
|
const diagrams = await listDiagrams();
|
||||||
@@ -104,6 +108,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
await updateConfig({ defaultDiagramId: diagram.id });
|
await updateConfig({ defaultDiagramId: diagram.id });
|
||||||
closeCreateDiagramDialog();
|
closeCreateDiagramDialog();
|
||||||
navigate(`/diagrams/${diagram.id}`);
|
navigate(`/diagrams/${diagram.id}`);
|
||||||
|
setTimeout(
|
||||||
|
() => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
|
||||||
|
700
|
||||||
|
);
|
||||||
}, [
|
}, [
|
||||||
databaseType,
|
databaseType,
|
||||||
addDiagram,
|
addDiagram,
|
||||||
@@ -112,6 +120,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
navigate,
|
navigate,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
diagramNumber,
|
diagramNumber,
|
||||||
|
openImportDBMLDialog,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -22,13 +22,17 @@ import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
|
|||||||
const ErrorMessageRelationshipFieldsNotSameType =
|
const ErrorMessageRelationshipFieldsNotSameType =
|
||||||
'Relationships can only be created between fields of the same type';
|
'Relationships can only be created between fields of the same type';
|
||||||
|
|
||||||
export interface CreateRelationshipDialogProps extends BaseDialogProps {}
|
export interface CreateRelationshipDialogProps extends BaseDialogProps {
|
||||||
|
sourceTableId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const CreateRelationshipDialog: React.FC<
|
export const CreateRelationshipDialog: React.FC<
|
||||||
CreateRelationshipDialogProps
|
CreateRelationshipDialogProps
|
||||||
> = ({ dialog }) => {
|
> = ({ dialog, sourceTableId: preSelectedSourceTableId }) => {
|
||||||
const { closeCreateRelationshipDialog } = useDialog();
|
const { closeCreateRelationshipDialog } = useDialog();
|
||||||
const [primaryTableId, setPrimaryTableId] = useState<string | undefined>();
|
const [primaryTableId, setPrimaryTableId] = useState<string | undefined>(
|
||||||
|
preSelectedSourceTableId
|
||||||
|
);
|
||||||
const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>();
|
const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>();
|
||||||
const [referencedTableId, setReferencedTableId] = useState<
|
const [referencedTableId, setReferencedTableId] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
@@ -43,6 +47,9 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
const [canCreateRelationship, setCanCreateRelationship] = useState(false);
|
const [canCreateRelationship, setCanCreateRelationship] = useState(false);
|
||||||
const { fitView, setEdges } = useReactFlow();
|
const { fitView, setEdges } = useReactFlow();
|
||||||
const { databaseType } = useChartDB();
|
const { databaseType } = useChartDB();
|
||||||
|
const [primaryFieldSelectOpen, setPrimaryFieldSelectOpen] = useState(false);
|
||||||
|
const [referencedTableSelectOpen, setReferencedTableSelectOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const tableOptions = useMemo(() => {
|
const tableOptions = useMemo(() => {
|
||||||
return tables.map(
|
return tables.map(
|
||||||
@@ -89,8 +96,23 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
setReferencedTableId(undefined);
|
setReferencedTableId(undefined);
|
||||||
setReferencedFieldId(undefined);
|
setReferencedFieldId(undefined);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
|
setPrimaryFieldSelectOpen(false);
|
||||||
|
setReferencedTableSelectOpen(false);
|
||||||
}, [dialog.open]);
|
}, [dialog.open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preSelectedSourceTableId) {
|
||||||
|
const table = getTable(preSelectedSourceTableId);
|
||||||
|
if (table) {
|
||||||
|
setPrimaryTableId(preSelectedSourceTableId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setPrimaryFieldSelectOpen(true);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [preSelectedSourceTableId, getTable]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCanCreateRelationship(false);
|
setCanCreateRelationship(false);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
@@ -223,8 +245,14 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
)}
|
)}
|
||||||
value={primaryTableId}
|
value={primaryTableId}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setPrimaryTableId(value as string);
|
const newTableId = value as string;
|
||||||
setPrimaryFieldId(undefined);
|
setPrimaryTableId(newTableId);
|
||||||
|
if (
|
||||||
|
newTableId !==
|
||||||
|
preSelectedSourceTableId
|
||||||
|
) {
|
||||||
|
setPrimaryFieldId(undefined);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
emptyPlaceholder={t(
|
emptyPlaceholder={t(
|
||||||
'create_relationship_dialog.no_tables_found'
|
'create_relationship_dialog.no_tables_found'
|
||||||
@@ -253,6 +281,8 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
'create_relationship_dialog.primary_field_placeholder'
|
'create_relationship_dialog.primary_field_placeholder'
|
||||||
)}
|
)}
|
||||||
value={primaryFieldId}
|
value={primaryFieldId}
|
||||||
|
open={primaryFieldSelectOpen}
|
||||||
|
onOpenChange={setPrimaryFieldSelectOpen}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setPrimaryFieldId(value as string)
|
setPrimaryFieldId(value as string)
|
||||||
}
|
}
|
||||||
@@ -283,6 +313,8 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
'create_relationship_dialog.referenced_table_placeholder'
|
'create_relationship_dialog.referenced_table_placeholder'
|
||||||
)}
|
)}
|
||||||
value={referencedTableId}
|
value={referencedTableId}
|
||||||
|
open={referencedTableSelectOpen}
|
||||||
|
onOpenChange={setReferencedTableSelectOpen}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setReferencedTableId(value as string);
|
setReferencedTableId(value as string);
|
||||||
setReferencedFieldId(undefined);
|
setReferencedFieldId(undefined);
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ import { SelectBox } from '@/components/select-box/select-box';
|
|||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { diagramToJSONOutput } from '@/lib/export-import-utils';
|
|
||||||
import { Spinner } from '@/components/spinner/spinner';
|
import { Spinner } from '@/components/spinner/spinner';
|
||||||
import { waitFor } from '@/lib/utils';
|
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
|
||||||
|
import { useExportDiagram } from '@/hooks/use-export-diagram';
|
||||||
|
|
||||||
export interface ExportDiagramDialogProps extends BaseDialogProps {}
|
export interface ExportDiagramDialogProps extends BaseDialogProps {}
|
||||||
|
|
||||||
@@ -27,44 +26,27 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
|
|||||||
dialog,
|
dialog,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { diagramName, currentDiagram } = useChartDB();
|
const { currentDiagram } = useChartDB();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { closeExportDiagramDialog } = useDialog();
|
const { closeExportDiagramDialog } = useDialog();
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dialog.open) return;
|
if (!dialog.open) return;
|
||||||
setIsLoading(false);
|
|
||||||
setError(false);
|
setError(false);
|
||||||
}, [dialog.open]);
|
}, [dialog.open]);
|
||||||
|
|
||||||
const downloadOutput = useCallback(
|
const { exportDiagram, isExporting: isLoading } = useExportDiagram();
|
||||||
(dataUrl: string) => {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.setAttribute('download', `ChartDB(${diagramName}).json`);
|
|
||||||
a.setAttribute('href', dataUrl);
|
|
||||||
a.click();
|
|
||||||
},
|
|
||||||
[diagramName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleExport = useCallback(async () => {
|
const handleExport = useCallback(async () => {
|
||||||
setIsLoading(true);
|
|
||||||
await waitFor(1000);
|
|
||||||
try {
|
try {
|
||||||
const json = diagramToJSONOutput(currentDiagram);
|
await exportDiagram({ diagram: currentDiagram });
|
||||||
const blob = new Blob([json], { type: 'application/json' });
|
|
||||||
const dataUrl = URL.createObjectURL(blob);
|
|
||||||
downloadOutput(dataUrl);
|
|
||||||
setIsLoading(false);
|
|
||||||
closeExportDiagramDialog();
|
closeExportDiagramDialog();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(true);
|
setError(true);
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
|
}, [exportDiagram, currentDiagram, closeExportDiagramDialog]);
|
||||||
|
|
||||||
const outputTypeOptions: SelectBoxOption[] = useMemo(
|
const outputTypeOptions: SelectBoxOption[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ import {
|
|||||||
} from '@/lib/data/export-metadata/export-sql-script';
|
} from '@/lib/data/export-metadata/export-sql-script';
|
||||||
import { databaseTypeToLabelMap } from '@/lib/databases';
|
import { databaseTypeToLabelMap } from '@/lib/databases';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
|
||||||
import { Annoyed, Sparkles } from 'lucide-react';
|
import { Annoyed, Sparkles } from 'lucide-react';
|
||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
|
||||||
export interface ExportSQLDialogProps extends BaseDialogProps {
|
export interface ExportSQLDialogProps extends BaseDialogProps {
|
||||||
targetDatabaseType: DatabaseType;
|
targetDatabaseType: DatabaseType;
|
||||||
@@ -34,7 +36,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
|||||||
targetDatabaseType,
|
targetDatabaseType,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeExportSQLDialog } = useDialog();
|
const { closeExportSQLDialog } = useDialog();
|
||||||
const { currentDiagram } = useChartDB();
|
const { currentDiagram, filteredSchemas } = useChartDB();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [script, setScript] = React.useState<string>();
|
const [script, setScript] = React.useState<string>();
|
||||||
const [error, setError] = React.useState<boolean>(false);
|
const [error, setError] = React.useState<boolean>(false);
|
||||||
@@ -43,17 +45,63 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
|||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const exportSQLScript = useCallback(async () => {
|
const exportSQLScript = useCallback(async () => {
|
||||||
|
const filteredDiagram: Diagram = {
|
||||||
|
...currentDiagram,
|
||||||
|
tables: currentDiagram.tables?.filter((table) =>
|
||||||
|
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
||||||
|
),
|
||||||
|
relationships: currentDiagram.relationships?.filter((rel) => {
|
||||||
|
const sourceTable = currentDiagram.tables?.find(
|
||||||
|
(t) => t.id === rel.sourceTableId
|
||||||
|
);
|
||||||
|
const targetTable = currentDiagram.tables?.find(
|
||||||
|
(t) => t.id === rel.targetTableId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
sourceTable &&
|
||||||
|
targetTable &&
|
||||||
|
shouldShowTablesBySchemaFilter(
|
||||||
|
sourceTable,
|
||||||
|
filteredSchemas
|
||||||
|
) &&
|
||||||
|
shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
dependencies: currentDiagram.dependencies?.filter((dep) => {
|
||||||
|
const table = currentDiagram.tables?.find(
|
||||||
|
(t) => t.id === dep.tableId
|
||||||
|
);
|
||||||
|
const dependentTable = currentDiagram.tables?.find(
|
||||||
|
(t) => t.id === dep.dependentTableId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
table &&
|
||||||
|
dependentTable &&
|
||||||
|
shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
|
||||||
|
shouldShowTablesBySchemaFilter(
|
||||||
|
dependentTable,
|
||||||
|
filteredSchemas
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
if (targetDatabaseType === DatabaseType.GENERIC) {
|
if (targetDatabaseType === DatabaseType.GENERIC) {
|
||||||
return Promise.resolve(exportBaseSQL(currentDiagram));
|
return Promise.resolve(
|
||||||
|
exportBaseSQL({
|
||||||
|
diagram: filteredDiagram,
|
||||||
|
targetDatabaseType,
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return exportSQL(currentDiagram, targetDatabaseType, {
|
return exportSQL(filteredDiagram, targetDatabaseType, {
|
||||||
stream: true,
|
stream: true,
|
||||||
onResultStream: (text) =>
|
onResultStream: (text) =>
|
||||||
setScript((prev) => (prev ? prev + text : text)),
|
setScript((prev) => (prev ? prev + text : text)),
|
||||||
signal: abortControllerRef.current?.signal,
|
signal: abortControllerRef.current?.signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [targetDatabaseType, currentDiagram]);
|
}, [targetDatabaseType, currentDiagram, filteredSchemas]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dialog.open) {
|
if (!dialog.open) {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
|||||||
DatabaseEdition | undefined
|
DatabaseEdition | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDatabaseEdition(undefined);
|
||||||
|
}, [databaseType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dialog.open) return;
|
if (!dialog.open) return;
|
||||||
setDatabaseEdition(undefined);
|
setDatabaseEdition(undefined);
|
||||||
|
|||||||
410
src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
Normal file
410
src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
Suspense,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import * as monaco from 'monaco-editor';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogInternalContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/dialog/dialog';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Editor } from '@/components/code-snippet/code-snippet';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
import { importDBMLToDiagram } from '@/lib/dbml-import';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { Parser } from '@dbml/core';
|
||||||
|
import { useCanvas } from '@/hooks/use-canvas';
|
||||||
|
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||||
|
import { useToast } from '@/components/toast/use-toast';
|
||||||
|
import { Spinner } from '@/components/spinner/spinner';
|
||||||
|
import { debounce } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface DBMLError {
|
||||||
|
message: string;
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDBMLError(error: unknown): DBMLError | null {
|
||||||
|
try {
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
const parsed = JSON.parse(error);
|
||||||
|
if (parsed.diags?.[0]) {
|
||||||
|
const diag = parsed.diags[0];
|
||||||
|
return {
|
||||||
|
message: diag.message,
|
||||||
|
line: diag.location.start.line,
|
||||||
|
column: diag.location.start.column,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (error && typeof error === 'object' && 'diags' in error) {
|
||||||
|
const parsed = error as {
|
||||||
|
diags: Array<{
|
||||||
|
message: string;
|
||||||
|
location: { start: { line: number; column: number } };
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
if (parsed.diags?.[0]) {
|
||||||
|
return {
|
||||||
|
message: parsed.diags[0].message,
|
||||||
|
line: parsed.diags[0].location.start.line,
|
||||||
|
column: parsed.diags[0].location.start.column,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing DBML error:', e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportDBMLDialogProps extends BaseDialogProps {
|
||||||
|
withCreateEmptyDiagram?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({
|
||||||
|
dialog,
|
||||||
|
withCreateEmptyDiagram,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const initialDBML = `// Use DBML to define your database structure
|
||||||
|
// Simple Blog System with Comments Example
|
||||||
|
|
||||||
|
Table users {
|
||||||
|
id integer [primary key]
|
||||||
|
name varchar
|
||||||
|
email varchar
|
||||||
|
}
|
||||||
|
|
||||||
|
Table posts {
|
||||||
|
id integer [primary key]
|
||||||
|
title varchar
|
||||||
|
content text
|
||||||
|
user_id integer
|
||||||
|
created_at timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
Table comments {
|
||||||
|
id integer [primary key]
|
||||||
|
content text
|
||||||
|
post_id integer
|
||||||
|
user_id integer
|
||||||
|
created_at timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
Ref: posts.user_id > users.id // Each post belongs to one user
|
||||||
|
Ref: comments.post_id > posts.id // Each comment belongs to one post
|
||||||
|
Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||||
|
|
||||||
|
const [dbmlContent, setDBMLContent] = useState<string>(initialDBML);
|
||||||
|
const { closeImportDBMLDialog } = useDialog();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||||
|
const { effectiveTheme } = useTheme();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const {
|
||||||
|
addTables,
|
||||||
|
addRelationships,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
removeTables,
|
||||||
|
removeRelationships,
|
||||||
|
} = useChartDB();
|
||||||
|
const { reorderTables } = useCanvas();
|
||||||
|
const [reorder, setReorder] = useState(false);
|
||||||
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
|
||||||
|
const decorationsCollection =
|
||||||
|
useRef<monaco.editor.IEditorDecorationsCollection>();
|
||||||
|
|
||||||
|
const handleEditorDidMount = (
|
||||||
|
editor: monaco.editor.IStandaloneCodeEditor
|
||||||
|
) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
decorationsCollection.current = editor.createDecorationsCollection();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reorder) {
|
||||||
|
reorderTables({
|
||||||
|
updateHistory: false,
|
||||||
|
});
|
||||||
|
setReorder(false);
|
||||||
|
}
|
||||||
|
}, [reorder, reorderTables]);
|
||||||
|
|
||||||
|
const highlightErrorLine = useCallback((error: DBMLError) => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
|
||||||
|
const model = editorRef.current.getModel();
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
const decorations = [
|
||||||
|
{
|
||||||
|
range: new monaco.Range(
|
||||||
|
error.line,
|
||||||
|
1,
|
||||||
|
error.line,
|
||||||
|
model.getLineMaxColumn(error.line)
|
||||||
|
),
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
className: 'dbml-error-line',
|
||||||
|
glyphMarginClassName: 'dbml-error-glyph',
|
||||||
|
hoverMessage: { value: error.message },
|
||||||
|
overviewRuler: {
|
||||||
|
color: '#ff0000',
|
||||||
|
position: monaco.editor.OverviewRulerLane.Right,
|
||||||
|
darkColor: '#ff0000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
decorationsCollection.current?.set(decorations);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearDecorations = useCallback(() => {
|
||||||
|
decorationsCollection.current?.clear();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const validateDBML = useCallback(
|
||||||
|
async (content: string) => {
|
||||||
|
// Clear previous errors
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
clearDecorations();
|
||||||
|
|
||||||
|
if (!content.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parser = new Parser();
|
||||||
|
parser.parse(content, 'dbml');
|
||||||
|
} catch (e) {
|
||||||
|
const parsedError = parseDBMLError(e);
|
||||||
|
if (parsedError) {
|
||||||
|
setErrorMessage(
|
||||||
|
t('import_dbml_dialog.error.description') +
|
||||||
|
` (1 error found - in line ${parsedError.line})`
|
||||||
|
);
|
||||||
|
highlightErrorLine(parsedError);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(
|
||||||
|
e instanceof Error ? e.message : JSON.stringify(e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[clearDecorations, highlightErrorLine, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
|
||||||
|
|
||||||
|
// Set up debounced validation
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedValidateRef.current = debounce((value: string) => {
|
||||||
|
validateDBML(value);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedValidateRef.current = null;
|
||||||
|
};
|
||||||
|
}, [validateDBML]);
|
||||||
|
|
||||||
|
// Trigger validation when content changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedValidateRef.current) {
|
||||||
|
debouncedValidateRef.current(dbmlContent);
|
||||||
|
}
|
||||||
|
}, [dbmlContent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialog.open) {
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
clearDecorations();
|
||||||
|
setDBMLContent(initialDBML);
|
||||||
|
}
|
||||||
|
}, [dialog.open, initialDBML, clearDecorations]);
|
||||||
|
|
||||||
|
const handleImport = useCallback(async () => {
|
||||||
|
if (!dbmlContent.trim() || errorMessage) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const importedDiagram = await importDBMLToDiagram(dbmlContent);
|
||||||
|
const tableIdsToRemove = tables
|
||||||
|
.filter((table) =>
|
||||||
|
importedDiagram.tables?.some(
|
||||||
|
(t) =>
|
||||||
|
t.name === table.name && t.schema === table.schema
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((table) => table.id);
|
||||||
|
// Find relationships that need to be removed
|
||||||
|
const relationshipIdsToRemove = relationships
|
||||||
|
.filter((relationship) => {
|
||||||
|
const sourceTable = tables.find(
|
||||||
|
(table) => table.id === relationship.sourceTableId
|
||||||
|
);
|
||||||
|
const targetTable = tables.find(
|
||||||
|
(table) => table.id === relationship.targetTableId
|
||||||
|
);
|
||||||
|
if (!sourceTable || !targetTable) return true;
|
||||||
|
const replacementSourceTable = importedDiagram.tables?.find(
|
||||||
|
(table) =>
|
||||||
|
table.name === sourceTable.name &&
|
||||||
|
table.schema === sourceTable.schema
|
||||||
|
);
|
||||||
|
const replacementTargetTable = importedDiagram.tables?.find(
|
||||||
|
(table) =>
|
||||||
|
table.name === targetTable.name &&
|
||||||
|
table.schema === targetTable.schema
|
||||||
|
);
|
||||||
|
return replacementSourceTable || replacementTargetTable;
|
||||||
|
})
|
||||||
|
.map((relationship) => relationship.id);
|
||||||
|
|
||||||
|
// Remove existing items
|
||||||
|
await Promise.all([
|
||||||
|
removeTables(tableIdsToRemove, { updateHistory: false }),
|
||||||
|
removeRelationships(relationshipIdsToRemove, {
|
||||||
|
updateHistory: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add new items
|
||||||
|
await Promise.all([
|
||||||
|
addTables(importedDiagram.tables ?? [], {
|
||||||
|
updateHistory: false,
|
||||||
|
}),
|
||||||
|
addRelationships(importedDiagram.relationships ?? [], {
|
||||||
|
updateHistory: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
setReorder(true);
|
||||||
|
closeImportDBMLDialog();
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t('import_dbml_dialog.error.title'),
|
||||||
|
variant: 'destructive',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
<div>{t('import_dbml_dialog.error.description')}</div>
|
||||||
|
{e instanceof Error ? e.message : JSON.stringify(e)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
dbmlContent,
|
||||||
|
closeImportDBMLDialog,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
removeTables,
|
||||||
|
removeRelationships,
|
||||||
|
addTables,
|
||||||
|
addRelationships,
|
||||||
|
errorMessage,
|
||||||
|
toast,
|
||||||
|
setReorder,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
{...dialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
closeImportDBMLDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[80vh] max-h-screen flex-col"
|
||||||
|
showClose
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{withCreateEmptyDiagram
|
||||||
|
? t('import_dbml_dialog.example_title')
|
||||||
|
: t('import_dbml_dialog.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('import_dbml_dialog.description')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogInternalContent>
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<Editor
|
||||||
|
value={dbmlContent}
|
||||||
|
onChange={(value) => setDBMLContent(value || '')}
|
||||||
|
language="dbml"
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
theme={
|
||||||
|
effectiveTheme === 'dark'
|
||||||
|
? 'dbml-dark'
|
||||||
|
: 'dbml-light'
|
||||||
|
}
|
||||||
|
beforeMount={setupDBMLLanguage}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
glyphMargin: true,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'visible',
|
||||||
|
horizontal: 'visible',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
className="size-full"
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</DialogInternalContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="secondary">
|
||||||
|
{withCreateEmptyDiagram
|
||||||
|
? t('import_dbml_dialog.skip_and_empty')
|
||||||
|
: t('import_dbml_dialog.cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
{errorMessage ? (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<AlertCircle className="size-4 text-destructive" />
|
||||||
|
|
||||||
|
<span className="text-xs text-destructive">
|
||||||
|
{errorMessage ||
|
||||||
|
t(
|
||||||
|
'import_dbml_dialog.error.description'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={!dbmlContent.trim() || !!errorMessage}
|
||||||
|
>
|
||||||
|
{withCreateEmptyDiagram
|
||||||
|
? t('import_dbml_dialog.show_example')
|
||||||
|
: t('import_dbml_dialog.import')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -22,15 +22,19 @@ import { useConfig } from '@/hooks/use-config';
|
|||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import { useStorage } from '@/hooks/use-storage';
|
import { useStorage } from '@/hooks/use-storage';
|
||||||
import type { Diagram } from '@/lib/domain/diagram';
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce';
|
||||||
|
|
||||||
export interface OpenDiagramDialogProps extends BaseDialogProps {}
|
export interface OpenDiagramDialogProps extends BaseDialogProps {
|
||||||
|
canClose?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||||
dialog,
|
dialog,
|
||||||
|
canClose = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeOpenDiagramDialog } = useDialog();
|
const { closeOpenDiagramDialog } = useDialog();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -58,24 +62,77 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
fetchDiagrams();
|
fetchDiagrams();
|
||||||
}, [listDiagrams, setDiagrams, dialog.open]);
|
}, [listDiagrams, setDiagrams, dialog.open]);
|
||||||
|
|
||||||
const openDiagram = (diagramId: string) => {
|
const openDiagram = useCallback(
|
||||||
if (diagramId) {
|
(diagramId: string) => {
|
||||||
updateConfig({ defaultDiagramId: diagramId });
|
if (diagramId) {
|
||||||
navigate(`/diagrams/${diagramId}`);
|
updateConfig({ defaultDiagramId: diagramId });
|
||||||
}
|
navigate(`/diagrams/${diagramId}`);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[updateConfig, navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLTableRowElement>) => {
|
||||||
|
const element = e.target as HTMLElement;
|
||||||
|
const diagramId = element.getAttribute('data-diagram-id');
|
||||||
|
const selectionIndexAttr = element.getAttribute(
|
||||||
|
'data-selection-index'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!diagramId || !selectionIndexAttr) return;
|
||||||
|
|
||||||
|
const selectionIndex = parseInt(selectionIndexAttr, 10);
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
e.preventDefault();
|
||||||
|
openDiagram(diagramId);
|
||||||
|
closeOpenDiagramDialog();
|
||||||
|
break;
|
||||||
|
case 'ArrowDown': {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
(
|
||||||
|
document.querySelector(
|
||||||
|
`[data-selection-index="${selectionIndex + 1}"]`
|
||||||
|
) as HTMLElement
|
||||||
|
)?.focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ArrowUp': {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
(
|
||||||
|
document.querySelector(
|
||||||
|
`[data-selection-index="${selectionIndex - 1}"]`
|
||||||
|
) as HTMLElement
|
||||||
|
)?.focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[openDiagram, closeOpenDiagramDialog]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFocusHandler = useDebounce(
|
||||||
|
(diagramId: string) => setSelectedDiagramId(diagramId),
|
||||||
|
50
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
{...dialog}
|
{...dialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open && canClose) {
|
||||||
closeOpenDiagramDialog();
|
closeOpenDiagramDialog();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]"
|
className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]"
|
||||||
showClose
|
showClose={canClose}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle>
|
<DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle>
|
||||||
@@ -112,10 +169,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{diagrams.map((diagram) => (
|
{diagrams.map((diagram, index) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={diagram.id}
|
key={diagram.id}
|
||||||
data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`}
|
data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`}
|
||||||
|
data-diagram-id={diagram.id}
|
||||||
|
data-selection-index={index}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() =>
|
||||||
|
onFocusHandler(diagram.id)
|
||||||
|
}
|
||||||
|
className="focus:bg-accent focus:outline-none"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
switch (e.detail) {
|
switch (e.detail) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -133,6 +197,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={handleRowKeyDown}
|
||||||
>
|
>
|
||||||
<TableCell className="table-cell">
|
<TableCell className="table-cell">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
@@ -164,11 +229,15 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
</DialogInternalContent>
|
</DialogInternalContent>
|
||||||
|
|
||||||
<DialogFooter className="flex !justify-between gap-2">
|
<DialogFooter className="flex !justify-between gap-2">
|
||||||
<DialogClose asChild>
|
{canClose ? (
|
||||||
<Button type="button" variant="secondary">
|
<DialogClose asChild>
|
||||||
{t('open_diagram_dialog.cancel')}
|
<Button type="button" variant="secondary">
|
||||||
</Button>
|
{t('open_diagram_dialog.cancel')}
|
||||||
</DialogClose>
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -30,6 +30,14 @@
|
|||||||
--chart-4: 43 74% 66%;
|
--chart-4: 43 74% 66%;
|
||||||
--chart-5: 27 87% 67%;
|
--chart-5: 27 87% 67%;
|
||||||
--subtitle: 215.3 19.3% 34.5%;
|
--subtitle: 215.3 19.3% 34.5%;
|
||||||
|
--sidebar-background: 0 0% 98%;
|
||||||
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
|
--sidebar-border: 220 13% 91%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -58,6 +66,14 @@
|
|||||||
--chart-4: 280 65% 60%;
|
--chart-4: 280 65% 60%;
|
||||||
--chart-5: 340 75% 55%;
|
--chart-5: 340 75% 55%;
|
||||||
--subtitle: 212.7 26.8% 83.9%;
|
--subtitle: 212.7 26.8% 83.9%;
|
||||||
|
--sidebar-background: 240 5.9% 10%;
|
||||||
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 240 3.7% 15.9%;
|
||||||
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +125,10 @@
|
|||||||
animation: rainbow-text-simple-animation 0.5s ease-in forwards;
|
animation: rainbow-text-simple-animation 0.5s ease-in forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dbml-error-line {
|
||||||
|
background-color: rgba(255, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes rainbow-text-simple-animation-rev {
|
@keyframes rainbow-text-simple-animation-rev {
|
||||||
0% {
|
0% {
|
||||||
background-size: 650%;
|
background-size: 650%;
|
||||||
|
|||||||
4
src/hooks/use-canvas.ts
Normal file
4
src/hooks/use-canvas.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { canvasContext } from '@/context/canvas-context/canvas-context';
|
||||||
|
|
||||||
|
export const useCanvas = () => useContext(canvasContext);
|
||||||
47
src/hooks/use-debounce-v2.ts
Normal file
47
src/hooks/use-debounce-v2.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { debounce as utilsDebounce } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface DebouncedFunction {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(...args: any[]): void;
|
||||||
|
cancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns a debounced version of the provided function.
|
||||||
|
* The debounced function will only be called after the specified delay
|
||||||
|
* has passed without the function being called again.
|
||||||
|
*
|
||||||
|
* @param callback The function to debounce
|
||||||
|
* @param delay The delay in milliseconds
|
||||||
|
* @returns A debounced version of the callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function useDebounce<T extends (...args: any[]) => any>(
|
||||||
|
callback: T,
|
||||||
|
delay: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
// Use a ref to store the debounced function
|
||||||
|
const debouncedFnRef = useRef<DebouncedFunction>();
|
||||||
|
|
||||||
|
// Update the debounced function when dependencies change
|
||||||
|
useEffect(() => {
|
||||||
|
// Create the debounced function
|
||||||
|
debouncedFnRef.current = utilsDebounce(callback, delay);
|
||||||
|
|
||||||
|
// Clean up when component unmounts or dependencies change
|
||||||
|
return () => {
|
||||||
|
if (debouncedFnRef.current?.cancel) {
|
||||||
|
debouncedFnRef.current.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [callback, delay]);
|
||||||
|
|
||||||
|
// Create a stable callback that uses the ref
|
||||||
|
const debouncedCallback = useCallback((...args: Parameters<T>) => {
|
||||||
|
debouncedFnRef.current?.(...args);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return debouncedCallback;
|
||||||
|
}
|
||||||
21
src/hooks/use-debounce.ts
Normal file
21
src/hooks/use-debounce.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type AnyFunction = (...args: any[]) => any;
|
||||||
|
|
||||||
|
export const useDebounce = <T extends AnyFunction>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): ((...args: Parameters<T>) => void) => {
|
||||||
|
const inDebounce = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const debounce = useCallback(
|
||||||
|
(...args: Parameters<T>) => {
|
||||||
|
clearTimeout(inDebounce.current);
|
||||||
|
inDebounce.current = setTimeout(() => func(...args), delay);
|
||||||
|
},
|
||||||
|
[func, delay]
|
||||||
|
);
|
||||||
|
|
||||||
|
return debounce;
|
||||||
|
};
|
||||||
40
src/hooks/use-export-diagram.tsx
Normal file
40
src/hooks/use-export-diagram.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import { diagramToJSONOutput } from '@/lib/export-import-utils';
|
||||||
|
import { waitFor } from '@/lib/utils';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
|
||||||
|
export const useExportDiagram = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { closeExportDiagramDialog } = useDialog();
|
||||||
|
|
||||||
|
const downloadOutput = useCallback((name: string, dataUrl: string) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('download', `ChartDB(${name}).json`);
|
||||||
|
a.setAttribute('href', dataUrl);
|
||||||
|
a.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleExport = useCallback(
|
||||||
|
async ({ diagram }: { diagram: Diagram }) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await waitFor(1000);
|
||||||
|
try {
|
||||||
|
const json = diagramToJSONOutput(diagram);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const dataUrl = URL.createObjectURL(blob);
|
||||||
|
downloadOutput(diagram.name, dataUrl);
|
||||||
|
setIsLoading(false);
|
||||||
|
closeExportDiagramDialog();
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[downloadOutput, closeExportDiagramDialog]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exportDiagram: handleExport,
|
||||||
|
isExporting: isLoading,
|
||||||
|
};
|
||||||
|
};
|
||||||
23
src/hooks/use-mobile.tsx
Normal file
23
src/hooks/use-mobile.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
export function useIsMobile() {
|
||||||
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const mql = window.matchMedia(
|
||||||
|
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`
|
||||||
|
);
|
||||||
|
const onChange = () => {
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
|
};
|
||||||
|
mql.addEventListener('change', onChange);
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
|
return () => mql.removeEventListener('change', onChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return !!isMobile;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ export const ar: LanguageTranslation = {
|
|||||||
new: 'جديد',
|
new: 'جديد',
|
||||||
open: 'فتح',
|
open: 'فتح',
|
||||||
save: 'حفظ',
|
save: 'حفظ',
|
||||||
import_database: 'استيراد قاعدة بيانات',
|
import: 'استيراد قاعدة بيانات',
|
||||||
export_sql: 'SQL تصدير',
|
export_sql: 'SQL تصدير',
|
||||||
export_as: 'تصدير كـ',
|
export_as: 'تصدير كـ',
|
||||||
delete_diagram: 'حذف الرسم البياني',
|
delete_diagram: 'حذف الرسم البياني',
|
||||||
@@ -34,13 +34,14 @@ export const ar: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
backup: {
|
||||||
share: 'مشاركة',
|
backup: 'النسخ الاحتياطي',
|
||||||
export_diagram: 'تصدير المخطط',
|
export_diagram: 'تصدير المخطط',
|
||||||
import_diagram: 'استيراد المخطط',
|
restore_diagram: 'استعادة المخطط',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'مساعدة',
|
help: 'مساعدة',
|
||||||
|
docs_website: 'الوثائق',
|
||||||
visit_website: 'ChartDB قم بزيارة',
|
visit_website: 'ChartDB قم بزيارة',
|
||||||
join_discord: 'Discord انضم إلينا على',
|
join_discord: 'Discord انضم إلينا على',
|
||||||
schedule_a_call: '!تحدث معنا',
|
schedule_a_call: '!تحدث معنا',
|
||||||
@@ -124,6 +125,12 @@ export const ar: LanguageTranslation = {
|
|||||||
add_table: 'إضافة جدول',
|
add_table: 'إضافة جدول',
|
||||||
filter: 'تصفية',
|
filter: 'تصفية',
|
||||||
collapse: 'طي الكل',
|
collapse: 'طي الكل',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'الحقول',
|
fields: 'الحقول',
|
||||||
@@ -144,6 +151,8 @@ export const ar: LanguageTranslation = {
|
|||||||
comments: 'تعليقات',
|
comments: 'تعليقات',
|
||||||
no_comments: 'لا يوجد تعليقات',
|
no_comments: 'لا يوجد تعليقات',
|
||||||
delete_field: 'حذف الحقل',
|
delete_field: 'حذف الحقل',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'خصائص الفهرس',
|
title: 'خصائص الفهرس',
|
||||||
@@ -358,7 +367,6 @@ export const ar: LanguageTranslation = {
|
|||||||
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
|
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
import_diagram_dialog: {
|
import_diagram_dialog: {
|
||||||
title: 'استيراد الرسم البياني',
|
title: 'استيراد الرسم البياني',
|
||||||
description: ':للرسم البياني ادناه JSON قم بلصق',
|
description: ':للرسم البياني ادناه JSON قم بلصق',
|
||||||
@@ -370,6 +378,20 @@ export const ar: LanguageTranslation = {
|
|||||||
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
|
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
import_dbml_dialog: {
|
||||||
|
// TODO: Translate
|
||||||
|
title: 'Import DBML',
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'واحد إلى واحد',
|
one_to_one: 'واحد إلى واحد',
|
||||||
one_to_many: 'واحد إلى متعدد',
|
one_to_many: 'واحد إلى متعدد',
|
||||||
@@ -386,6 +408,7 @@ export const ar: LanguageTranslation = {
|
|||||||
edit_table: 'تعديل الجدول',
|
edit_table: 'تعديل الجدول',
|
||||||
duplicate_table: 'نسخ الجدول',
|
duplicate_table: 'نسخ الجدول',
|
||||||
delete_table: 'حذف الجدول',
|
delete_table: 'حذف الجدول',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على',
|
snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const bn: LanguageTranslation = {
|
|||||||
new: 'নতুন',
|
new: 'নতুন',
|
||||||
open: 'খুলুন',
|
open: 'খুলুন',
|
||||||
save: 'সংরক্ষণ করুন',
|
save: 'সংরক্ষণ করুন',
|
||||||
import_database: 'ডাটাবেস আমদানি করুন',
|
import: 'ডাটাবেস আমদানি করুন',
|
||||||
export_sql: 'SQL রপ্তানি করুন',
|
export_sql: 'SQL রপ্তানি করুন',
|
||||||
export_as: 'রূপে রপ্তানি করুন',
|
export_as: 'রূপে রপ্তানি করুন',
|
||||||
delete_diagram: 'ডায়াগ্রাম মুছুন',
|
delete_diagram: 'ডায়াগ্রাম মুছুন',
|
||||||
@@ -35,13 +35,14 @@ export const bn: LanguageTranslation = {
|
|||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
|
|
||||||
share: {
|
backup: {
|
||||||
share: 'শেয়ার করুন',
|
backup: 'ব্যাকআপ',
|
||||||
export_diagram: 'ডায়াগ্রাম রপ্তানি করুন',
|
export_diagram: 'ডায়াগ্রাম রপ্তানি করুন',
|
||||||
import_diagram: 'ডায়াগ্রাম আমদানি করুন',
|
restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'সাহায্য',
|
help: 'সাহায্য',
|
||||||
|
docs_website: 'ডকুমেন্টেশন',
|
||||||
visit_website: 'ChartDB ওয়েবসাইটে যান',
|
visit_website: 'ChartDB ওয়েবসাইটে যান',
|
||||||
join_discord: 'আমাদের Discord-এ যোগ দিন',
|
join_discord: 'আমাদের Discord-এ যোগ দিন',
|
||||||
schedule_a_call: 'আমাদের সাথে কথা বলুন!',
|
schedule_a_call: 'আমাদের সাথে কথা বলুন!',
|
||||||
@@ -125,6 +126,12 @@ export const bn: LanguageTranslation = {
|
|||||||
add_table: 'টেবিল যোগ করুন',
|
add_table: 'টেবিল যোগ করুন',
|
||||||
filter: 'ফিল্টার',
|
filter: 'ফিল্টার',
|
||||||
collapse: 'সব ভাঁজ করুন',
|
collapse: 'সব ভাঁজ করুন',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'ফিল্ড',
|
fields: 'ফিল্ড',
|
||||||
@@ -145,6 +152,8 @@ export const bn: LanguageTranslation = {
|
|||||||
comments: 'মন্তব্য',
|
comments: 'মন্তব্য',
|
||||||
no_comments: 'কোনো মন্তব্য নেই',
|
no_comments: 'কোনো মন্তব্য নেই',
|
||||||
delete_field: 'ফিল্ড মুছুন',
|
delete_field: 'ফিল্ড মুছুন',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'ইনডেক্স কর্ম',
|
title: 'ইনডেক্স কর্ম',
|
||||||
@@ -373,6 +382,20 @@ export const bn: LanguageTranslation = {
|
|||||||
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
|
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'এক থেকে এক',
|
one_to_one: 'এক থেকে এক',
|
||||||
one_to_many: 'এক থেকে অনেক',
|
one_to_many: 'এক থেকে অনেক',
|
||||||
@@ -389,6 +412,7 @@ export const bn: LanguageTranslation = {
|
|||||||
edit_table: 'টেবিল সম্পাদনা করুন',
|
edit_table: 'টেবিল সম্পাদনা করুন',
|
||||||
duplicate_table: 'টেবিল নকল করুন',
|
duplicate_table: 'টেবিল নকল করুন',
|
||||||
delete_table: 'টেবিল মুছে ফেলুন',
|
delete_table: 'টেবিল মুছে ফেলুন',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})',
|
snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const de: LanguageTranslation = {
|
|||||||
new: 'Neu',
|
new: 'Neu',
|
||||||
open: 'Öffnen',
|
open: 'Öffnen',
|
||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
import_database: 'Datenbank importieren',
|
import: 'Datenbank importieren',
|
||||||
export_sql: 'SQL exportieren',
|
export_sql: 'SQL exportieren',
|
||||||
export_as: 'Exportieren als',
|
export_as: 'Exportieren als',
|
||||||
delete_diagram: 'Diagramm löschen',
|
delete_diagram: 'Diagramm löschen',
|
||||||
@@ -35,13 +35,14 @@ export const de: LanguageTranslation = {
|
|||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
backup: {
|
||||||
share: 'Share',
|
backup: 'Backup',
|
||||||
export_diagram: 'Export Diagram',
|
export_diagram: 'Export Diagram',
|
||||||
import_diagram: 'Import Diagram',
|
restore_diagram: 'Restore Diagram',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Hilfe',
|
help: 'Hilfe',
|
||||||
|
docs_website: 'Dokumentation',
|
||||||
visit_website: 'ChartDB Webseite',
|
visit_website: 'ChartDB Webseite',
|
||||||
join_discord: 'Auf Discord beitreten',
|
join_discord: 'Auf Discord beitreten',
|
||||||
schedule_a_call: 'Gespräch vereinbaren',
|
schedule_a_call: 'Gespräch vereinbaren',
|
||||||
@@ -126,6 +127,12 @@ export const de: LanguageTranslation = {
|
|||||||
add_table: 'Tabelle hinzufügen',
|
add_table: 'Tabelle hinzufügen',
|
||||||
filter: 'Filter',
|
filter: 'Filter',
|
||||||
collapse: 'Alle einklappen',
|
collapse: 'Alle einklappen',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Felder',
|
fields: 'Felder',
|
||||||
@@ -146,6 +153,8 @@ export const de: LanguageTranslation = {
|
|||||||
comments: 'Kommentare',
|
comments: 'Kommentare',
|
||||||
no_comments: 'Keine Kommentare',
|
no_comments: 'Keine Kommentare',
|
||||||
delete_field: 'Feld löschen',
|
delete_field: 'Feld löschen',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Indexattribute',
|
title: 'Indexattribute',
|
||||||
@@ -376,6 +385,20 @@ export const de: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Ein zu Eins (1:1)',
|
one_to_one: 'Ein zu Eins (1:1)',
|
||||||
one_to_many: 'Ein zu Viele (1:n)',
|
one_to_many: 'Ein zu Viele (1:n)',
|
||||||
@@ -392,6 +415,7 @@ export const de: LanguageTranslation = {
|
|||||||
edit_table: 'Tabelle bearbeiten',
|
edit_table: 'Tabelle bearbeiten',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
delete_table: 'Tabelle löschen',
|
delete_table: 'Tabelle löschen',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Add translations
|
// TODO: Add translations
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const en = {
|
|||||||
new: 'New',
|
new: 'New',
|
||||||
open: 'Open',
|
open: 'Open',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
import_database: 'Import Database',
|
import: 'Import',
|
||||||
export_sql: 'Export SQL',
|
export_sql: 'Export SQL',
|
||||||
export_as: 'Export as',
|
export_as: 'Export as',
|
||||||
delete_diagram: 'Delete Diagram',
|
delete_diagram: 'Delete Diagram',
|
||||||
@@ -33,13 +33,14 @@ export const en = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
backup: {
|
||||||
share: 'Share',
|
backup: 'Backup',
|
||||||
export_diagram: 'Export Diagram',
|
export_diagram: 'Export Diagram',
|
||||||
import_diagram: 'Import Diagram',
|
restore_diagram: 'Restore Diagram',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Help',
|
help: 'Help',
|
||||||
|
docs_website: 'Docs',
|
||||||
visit_website: 'Visit ChartDB',
|
visit_website: 'Visit ChartDB',
|
||||||
join_discord: 'Join us on Discord',
|
join_discord: 'Join us on Discord',
|
||||||
schedule_a_call: 'Talk with us!',
|
schedule_a_call: 'Talk with us!',
|
||||||
@@ -123,6 +124,10 @@ export const en = {
|
|||||||
add_table: 'Add Table',
|
add_table: 'Add Table',
|
||||||
filter: 'Filter',
|
filter: 'Filter',
|
||||||
collapse: 'Collapse All',
|
collapse: 'Collapse All',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Fields',
|
fields: 'Fields',
|
||||||
@@ -140,6 +145,7 @@ export const en = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Field Attributes',
|
title: 'Field Attributes',
|
||||||
unique: 'Unique',
|
unique: 'Unique',
|
||||||
|
character_length: 'Max Length',
|
||||||
comments: 'Comments',
|
comments: 'Comments',
|
||||||
no_comments: 'No comments',
|
no_comments: 'No comments',
|
||||||
delete_field: 'Delete Field',
|
delete_field: 'Delete Field',
|
||||||
@@ -361,7 +367,7 @@ export const en = {
|
|||||||
|
|
||||||
import_diagram_dialog: {
|
import_diagram_dialog: {
|
||||||
title: 'Import Diagram',
|
title: 'Import Diagram',
|
||||||
description: 'Paste the diagram JSON below:',
|
description: 'Import a diagram from a JSON file.',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
import: 'Import',
|
import: 'Import',
|
||||||
error: {
|
error: {
|
||||||
@@ -370,6 +376,20 @@ export const en = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error importing DBML',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'One to One',
|
one_to_one: 'One to One',
|
||||||
one_to_many: 'One to Many',
|
one_to_many: 'One to Many',
|
||||||
@@ -386,6 +406,7 @@ export const en = {
|
|||||||
edit_table: 'Edit Table',
|
edit_table: 'Edit Table',
|
||||||
duplicate_table: 'Duplicate Table',
|
duplicate_table: 'Duplicate Table',
|
||||||
delete_table: 'Delete Table',
|
delete_table: 'Delete Table',
|
||||||
|
add_relationship: 'Add Relationship',
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const es: LanguageTranslation = {
|
|||||||
new: 'Nuevo',
|
new: 'Nuevo',
|
||||||
open: 'Abrir',
|
open: 'Abrir',
|
||||||
save: 'Guardar',
|
save: 'Guardar',
|
||||||
import_database: 'Importar Base de Datos',
|
import: 'Importar Base de Datos',
|
||||||
export_sql: 'Exportar SQL',
|
export_sql: 'Exportar SQL',
|
||||||
export_as: 'Exportar como',
|
export_as: 'Exportar como',
|
||||||
delete_diagram: 'Eliminar Diagrama',
|
delete_diagram: 'Eliminar Diagrama',
|
||||||
@@ -34,14 +34,14 @@ export const es: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
backup: {
|
||||||
share: {
|
backup: 'Respaldo',
|
||||||
share: 'Share',
|
export_diagram: 'Exportar Diagrama',
|
||||||
export_diagram: 'Export Diagram',
|
restore_diagram: 'Restaurar Diagrama',
|
||||||
import_diagram: 'Import Diagram',
|
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Ayuda',
|
help: 'Ayuda',
|
||||||
|
docs_website: 'Documentación',
|
||||||
visit_website: 'Visitar ChartDB',
|
visit_website: 'Visitar ChartDB',
|
||||||
join_discord: 'Únete a nosotros en Discord',
|
join_discord: 'Únete a nosotros en Discord',
|
||||||
schedule_a_call: '¡Habla con nosotros!',
|
schedule_a_call: '¡Habla con nosotros!',
|
||||||
@@ -116,6 +116,12 @@ export const es: LanguageTranslation = {
|
|||||||
add_table: 'Agregar Tabla',
|
add_table: 'Agregar Tabla',
|
||||||
filter: 'Filtrar',
|
filter: 'Filtrar',
|
||||||
collapse: 'Colapsar Todo',
|
collapse: 'Colapsar Todo',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Campos',
|
fields: 'Campos',
|
||||||
@@ -136,6 +142,8 @@ export const es: LanguageTranslation = {
|
|||||||
comments: 'Comentarios',
|
comments: 'Comentarios',
|
||||||
no_comments: 'Sin comentarios',
|
no_comments: 'Sin comentarios',
|
||||||
delete_field: 'Eliminar Campo',
|
delete_field: 'Eliminar Campo',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Atributos del Índice',
|
title: 'Atributos del Índice',
|
||||||
@@ -375,6 +383,20 @@ export const es: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Uno a Uno',
|
one_to_one: 'Uno a Uno',
|
||||||
one_to_many: 'Uno a Muchos',
|
one_to_many: 'Uno a Muchos',
|
||||||
@@ -391,6 +413,7 @@ export const es: LanguageTranslation = {
|
|||||||
edit_table: 'Editar Tabla',
|
edit_table: 'Editar Tabla',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
delete_table: 'Eliminar Tabla',
|
delete_table: 'Eliminar Tabla',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Add translations
|
// TODO: Add translations
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const fr: LanguageTranslation = {
|
|||||||
new: 'Nouveau',
|
new: 'Nouveau',
|
||||||
open: 'Ouvrir',
|
open: 'Ouvrir',
|
||||||
save: 'Enregistrer',
|
save: 'Enregistrer',
|
||||||
import_database: 'Importer Base de Données',
|
import: 'Importer Base de Données',
|
||||||
export_sql: 'Exporter SQL',
|
export_sql: 'Exporter SQL',
|
||||||
export_as: 'Exporter en tant que',
|
export_as: 'Exporter en tant que',
|
||||||
delete_diagram: 'Supprimer le Diagramme',
|
delete_diagram: 'Supprimer le Diagramme',
|
||||||
@@ -30,17 +30,17 @@ export const fr: LanguageTranslation = {
|
|||||||
theme: 'Thème',
|
theme: 'Thème',
|
||||||
show_dependencies: 'Afficher les Dépendances',
|
show_dependencies: 'Afficher les Dépendances',
|
||||||
hide_dependencies: 'Masquer les Dépendances',
|
hide_dependencies: 'Masquer les Dépendances',
|
||||||
// TODO: Translate
|
show_minimap: 'Afficher la Mini Carte',
|
||||||
show_minimap: 'Show Mini Map',
|
hide_minimap: 'Masquer la Mini Carte',
|
||||||
hide_minimap: 'Hide Mini Map',
|
|
||||||
},
|
},
|
||||||
share: {
|
backup: {
|
||||||
share: 'Partage',
|
backup: 'Sauvegarde',
|
||||||
export_diagram: 'Exporter le diagramme',
|
export_diagram: 'Exporter le diagramme',
|
||||||
import_diagram: 'Importer un diagramme',
|
restore_diagram: 'Restaurer le diagramme',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Aide',
|
help: 'Aide',
|
||||||
|
docs_website: 'Documentation',
|
||||||
visit_website: 'Visitez ChartDB',
|
visit_website: 'Visitez ChartDB',
|
||||||
join_discord: 'Rejoignez-nous sur Discord',
|
join_discord: 'Rejoignez-nous sur Discord',
|
||||||
schedule_a_call: 'Parlez avec nous !',
|
schedule_a_call: 'Parlez avec nous !',
|
||||||
@@ -101,9 +101,8 @@ export const fr: LanguageTranslation = {
|
|||||||
clear: 'Effacer',
|
clear: 'Effacer',
|
||||||
show_more: 'Afficher Plus',
|
show_more: 'Afficher Plus',
|
||||||
show_less: 'Afficher Moins',
|
show_less: 'Afficher Moins',
|
||||||
// TODO: Translate
|
copy_to_clipboard: 'Copier dans le presse-papiers',
|
||||||
copy_to_clipboard: 'Copy to Clipboard',
|
copied: 'Copié !',
|
||||||
copied: 'Copied!',
|
|
||||||
|
|
||||||
side_panel: {
|
side_panel: {
|
||||||
schema: 'Schéma:',
|
schema: 'Schéma:',
|
||||||
@@ -116,6 +115,11 @@ export const fr: LanguageTranslation = {
|
|||||||
add_table: 'Ajouter une Table',
|
add_table: 'Ajouter une Table',
|
||||||
filter: 'Filtrer',
|
filter: 'Filtrer',
|
||||||
collapse: 'Réduire Tout',
|
collapse: 'Réduire Tout',
|
||||||
|
clear: 'Effacer le Filtre',
|
||||||
|
no_results:
|
||||||
|
'Aucune table trouvée correspondant à votre filtre.',
|
||||||
|
show_list: 'Afficher la Liste des Tableaux',
|
||||||
|
show_dbml: "Afficher l'éditeur DBML",
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Champs',
|
fields: 'Champs',
|
||||||
@@ -136,6 +140,8 @@ export const fr: LanguageTranslation = {
|
|||||||
comments: 'Commentaires',
|
comments: 'Commentaires',
|
||||||
no_comments: 'Pas de commentaires',
|
no_comments: 'Pas de commentaires',
|
||||||
delete_field: 'Supprimer le Champ',
|
delete_field: 'Supprimer le Champ',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: "Attributs de l'Index",
|
title: "Attributs de l'Index",
|
||||||
@@ -147,7 +153,7 @@ export const fr: LanguageTranslation = {
|
|||||||
title: 'Actions de la Table',
|
title: 'Actions de la Table',
|
||||||
add_field: 'Ajouter un Champ',
|
add_field: 'Ajouter un Champ',
|
||||||
add_index: 'Ajouter un Index',
|
add_index: 'Ajouter un Index',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Tableau dupliqué',
|
||||||
delete_table: 'Supprimer la Table',
|
delete_table: 'Supprimer la Table',
|
||||||
change_schema: 'Changer le Schéma',
|
change_schema: 'Changer le Schéma',
|
||||||
},
|
},
|
||||||
@@ -230,14 +236,12 @@ export const fr: LanguageTranslation = {
|
|||||||
step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).',
|
step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).',
|
||||||
},
|
},
|
||||||
instructions_link: "Besoin d'aide ? Regardez comment",
|
instructions_link: "Besoin d'aide ? Regardez comment",
|
||||||
// TODO: Translate
|
check_script_result: 'Vérifier le résultat du Script',
|
||||||
check_script_result: 'Check Script Result',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
back: 'Retour',
|
back: 'Retour',
|
||||||
// TODO: Translate
|
import_from_file: "Importer à partir d'un fichier",
|
||||||
import_from_file: 'Import from File',
|
|
||||||
empty_diagram: 'Diagramme vide',
|
empty_diagram: 'Diagramme vide',
|
||||||
continue: 'Continuer',
|
continue: 'Continuer',
|
||||||
import: 'Importer',
|
import: 'Importer',
|
||||||
@@ -352,29 +356,42 @@ export const fr: LanguageTranslation = {
|
|||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
|
||||||
export_diagram_dialog: {
|
export_diagram_dialog: {
|
||||||
title: 'Export Diagram',
|
title: 'Exporter le Diagramme',
|
||||||
description: 'Choose the format for export:',
|
description: "Sélectionner le format d'exportation :",
|
||||||
format_json: 'JSON',
|
format_json: 'JSON',
|
||||||
cancel: 'Cancel',
|
cancel: 'Annuler',
|
||||||
export: 'Export',
|
export: 'Exporter',
|
||||||
error: {
|
error: {
|
||||||
title: 'Error exporting diagram',
|
title: "Erreur lors de l'exportation du diagramme",
|
||||||
description:
|
description:
|
||||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
"Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
|
||||||
import_diagram_dialog: {
|
import_diagram_dialog: {
|
||||||
title: 'Import Diagram',
|
title: 'Importer un diagramme',
|
||||||
description: 'Paste the diagram JSON below:',
|
description: 'Coller le diagramme au format JSON ci-dessous :',
|
||||||
cancel: 'Cancel',
|
cancel: 'Annuler',
|
||||||
import: 'Import',
|
import: 'Exporter',
|
||||||
error: {
|
error: {
|
||||||
title: 'Error importing diagram',
|
title: "Erreur lors de l'exportation du diagramme",
|
||||||
description:
|
description:
|
||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? chartdb.io@gmail.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: "Exemple d'importation DBML",
|
||||||
|
title: 'Import DBML',
|
||||||
|
description:
|
||||||
|
'Importer un schéma de base de données à partir du format DBML.',
|
||||||
|
import: 'Importer',
|
||||||
|
cancel: 'Annuler',
|
||||||
|
skip_and_empty: 'Passer et vider',
|
||||||
|
show_example: 'Afficher un exemple',
|
||||||
|
error: {
|
||||||
|
title: 'Erreur',
|
||||||
|
description:
|
||||||
|
"Erreur d'analyse du DBML. Veuillez vérifier la syntaxe.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
@@ -391,12 +408,13 @@ export const fr: LanguageTranslation = {
|
|||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
edit_table: 'Éditer la Table',
|
edit_table: 'Éditer la Table',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Tableau Dupliqué',
|
||||||
delete_table: 'Supprimer la Table',
|
delete_table: 'Supprimer la Table',
|
||||||
|
add_relationship: 'Ajouter une Relation',
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Add translations
|
snap_to_grid_tooltip:
|
||||||
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
'Aligner sur la grille (maintenir la touche {{key}})',
|
||||||
|
|
||||||
tool_tips: {
|
tool_tips: {
|
||||||
double_click_to_edit: 'Double-cliquez pour modifier',
|
double_click_to_edit: 'Double-cliquez pour modifier',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const gu: LanguageTranslation = {
|
|||||||
new: 'નવું',
|
new: 'નવું',
|
||||||
open: 'ખોલો',
|
open: 'ખોલો',
|
||||||
save: 'સાચવો',
|
save: 'સાચવો',
|
||||||
import_database: 'ડેટાબેસ આયાત કરો',
|
import: 'ડેટાબેસ આયાત કરો',
|
||||||
export_sql: 'SQL નિકાસ કરો',
|
export_sql: 'SQL નિકાસ કરો',
|
||||||
export_as: 'રૂપે નિકાસ કરો',
|
export_as: 'રૂપે નિકાસ કરો',
|
||||||
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
|
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
|
||||||
@@ -35,13 +35,14 @@ export const gu: LanguageTranslation = {
|
|||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
|
|
||||||
share: {
|
backup: {
|
||||||
share: 'શેર કરો',
|
backup: 'બેકઅપ',
|
||||||
export_diagram: 'ડાયાગ્રામ નિકાસ કરો',
|
export_diagram: 'ડાયાગ્રામ નિકાસ કરો',
|
||||||
import_diagram: 'ડાયાગ્રામ આયાત કરો',
|
restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'મદદ',
|
help: 'મદદ',
|
||||||
|
docs_website: 'દસ્તાવેજીકરણ',
|
||||||
visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
|
visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
|
||||||
join_discord: 'અમારા Discordમાં જોડાઓ',
|
join_discord: 'અમારા Discordમાં જોડાઓ',
|
||||||
schedule_a_call: 'અમારી સાથે વાત કરો!',
|
schedule_a_call: 'અમારી સાથે વાત કરો!',
|
||||||
@@ -125,6 +126,12 @@ export const gu: LanguageTranslation = {
|
|||||||
add_table: 'ટેબલ ઉમેરો',
|
add_table: 'ટેબલ ઉમેરો',
|
||||||
filter: 'ફિલ્ટર',
|
filter: 'ફિલ્ટર',
|
||||||
collapse: 'બધાને સકુચિત કરો',
|
collapse: 'બધાને સકુચિત કરો',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'ફીલ્ડ્સ',
|
fields: 'ફીલ્ડ્સ',
|
||||||
@@ -146,6 +153,8 @@ export const gu: LanguageTranslation = {
|
|||||||
comments: 'ટિપ્પણીઓ',
|
comments: 'ટિપ્પણીઓ',
|
||||||
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
||||||
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'ઇન્ડેક્સ લક્ષણો',
|
title: 'ઇન્ડેક્સ લક્ષણો',
|
||||||
@@ -373,6 +382,20 @@ export const gu: LanguageTranslation = {
|
|||||||
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
|
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'એકથી એક',
|
one_to_one: 'એકથી એક',
|
||||||
one_to_many: 'એકથી ઘણા',
|
one_to_many: 'એકથી ઘણા',
|
||||||
@@ -389,6 +412,7 @@ export const gu: LanguageTranslation = {
|
|||||||
edit_table: 'ટેબલ સંપાદિત કરો',
|
edit_table: 'ટેબલ સંપાદિત કરો',
|
||||||
duplicate_table: 'ટેબલ નકલ કરો',
|
duplicate_table: 'ટેબલ નકલ કરો',
|
||||||
delete_table: 'ટેબલ કાઢી નાખો',
|
delete_table: 'ટેબલ કાઢી નાખો',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})',
|
snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const hi: LanguageTranslation = {
|
|||||||
new: 'नया',
|
new: 'नया',
|
||||||
open: 'खोलें',
|
open: 'खोलें',
|
||||||
save: 'सहेजें',
|
save: 'सहेजें',
|
||||||
import_database: 'डेटाबेस आयात करें',
|
import: 'डेटाबेस आयात करें',
|
||||||
export_sql: 'SQL निर्यात करें',
|
export_sql: 'SQL निर्यात करें',
|
||||||
export_as: 'के रूप में निर्यात करें',
|
export_as: 'के रूप में निर्यात करें',
|
||||||
delete_diagram: 'आरेख हटाएँ',
|
delete_diagram: 'आरेख हटाएँ',
|
||||||
@@ -34,14 +34,14 @@ export const hi: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
backup: {
|
||||||
share: {
|
backup: 'बैकअप',
|
||||||
share: 'Share',
|
export_diagram: 'आरेख निर्यात करें',
|
||||||
export_diagram: 'Export Diagram',
|
restore_diagram: 'आरेख पुनर्स्थापित करें',
|
||||||
import_diagram: 'Import Diagram',
|
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'मदद',
|
help: 'मदद',
|
||||||
|
docs_website: 'દસ્તાવેજીકરણ',
|
||||||
visit_website: 'ChartDB वेबसाइट पर जाएँ',
|
visit_website: 'ChartDB वेबसाइट पर जाएँ',
|
||||||
join_discord: 'हमसे Discord पर जुड़ें',
|
join_discord: 'हमसे Discord पर जुड़ें',
|
||||||
schedule_a_call: 'हमसे बात करें!',
|
schedule_a_call: 'हमसे बात करें!',
|
||||||
@@ -126,6 +126,12 @@ export const hi: LanguageTranslation = {
|
|||||||
add_table: 'तालिका जोड़ें',
|
add_table: 'तालिका जोड़ें',
|
||||||
filter: 'फ़िल्टर',
|
filter: 'फ़िल्टर',
|
||||||
collapse: 'सभी को संक्षिप्त करें',
|
collapse: 'सभी को संक्षिप्त करें',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'फ़ील्ड्स',
|
fields: 'फ़ील्ड्स',
|
||||||
@@ -146,6 +152,8 @@ export const hi: LanguageTranslation = {
|
|||||||
comments: 'टिप्पणियाँ',
|
comments: 'टिप्पणियाँ',
|
||||||
no_comments: 'कोई टिप्पणी नहीं',
|
no_comments: 'कोई टिप्पणी नहीं',
|
||||||
delete_field: 'फ़ील्ड हटाएँ',
|
delete_field: 'फ़ील्ड हटाएँ',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'सूचकांक विशेषताएँ',
|
title: 'सूचकांक विशेषताएँ',
|
||||||
@@ -377,6 +385,20 @@ export const hi: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'एक से एक',
|
one_to_one: 'एक से एक',
|
||||||
one_to_many: 'एक से कई',
|
one_to_many: 'एक से कई',
|
||||||
@@ -393,6 +415,7 @@ export const hi: LanguageTranslation = {
|
|||||||
edit_table: 'तालिका संपादित करें',
|
edit_table: 'तालिका संपादित करें',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
delete_table: 'तालिका हटाएँ',
|
delete_table: 'तालिका हटाएँ',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Add translations
|
// TODO: Add translations
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const id_ID: LanguageTranslation = {
|
|||||||
new: 'Buat Baru',
|
new: 'Buat Baru',
|
||||||
open: 'Buka',
|
open: 'Buka',
|
||||||
save: 'Simpan',
|
save: 'Simpan',
|
||||||
import_database: 'Impor Database',
|
import: 'Impor Database',
|
||||||
export_sql: 'Ekspor SQL',
|
export_sql: 'Ekspor SQL',
|
||||||
export_as: 'Ekspor Sebagai',
|
export_as: 'Ekspor Sebagai',
|
||||||
delete_diagram: 'Hapus Diagram',
|
delete_diagram: 'Hapus Diagram',
|
||||||
@@ -34,13 +34,14 @@ export const id_ID: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
backup: {
|
||||||
share: 'Bagikan',
|
backup: 'Cadangan',
|
||||||
export_diagram: 'Ekspor Diagram',
|
export_diagram: 'Ekspor Diagram',
|
||||||
import_diagram: 'Impor Diagram',
|
restore_diagram: 'Pulihkan Diagram',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Bantuan',
|
help: 'Bantuan',
|
||||||
|
docs_website: 'દસ્તાવેજીકરણ',
|
||||||
visit_website: 'Kunjungi ChartDB',
|
visit_website: 'Kunjungi ChartDB',
|
||||||
join_discord: 'Bergabunglah di Discord kami',
|
join_discord: 'Bergabunglah di Discord kami',
|
||||||
schedule_a_call: 'Berbicara dengan kami!',
|
schedule_a_call: 'Berbicara dengan kami!',
|
||||||
@@ -124,6 +125,12 @@ export const id_ID: LanguageTranslation = {
|
|||||||
add_table: 'Tambah Tabel',
|
add_table: 'Tambah Tabel',
|
||||||
filter: 'Saring',
|
filter: 'Saring',
|
||||||
collapse: 'Lipat Semua',
|
collapse: 'Lipat Semua',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Kolom',
|
fields: 'Kolom',
|
||||||
@@ -144,6 +151,8 @@ export const id_ID: LanguageTranslation = {
|
|||||||
comments: 'Komentar',
|
comments: 'Komentar',
|
||||||
no_comments: 'Tidak ada komentar',
|
no_comments: 'Tidak ada komentar',
|
||||||
delete_field: 'Hapus Kolom',
|
delete_field: 'Hapus Kolom',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Atribut Indeks',
|
title: 'Atribut Indeks',
|
||||||
@@ -371,6 +380,20 @@ export const id_ID: LanguageTranslation = {
|
|||||||
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
|
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Satu ke Satu',
|
one_to_one: 'Satu ke Satu',
|
||||||
@@ -388,6 +411,7 @@ export const id_ID: LanguageTranslation = {
|
|||||||
edit_table: 'Ubah Tabel',
|
edit_table: 'Ubah Tabel',
|
||||||
delete_table: 'Hapus Tabel',
|
delete_table: 'Hapus Tabel',
|
||||||
duplicate_table: 'Duplikat Tabel',
|
duplicate_table: 'Duplikat Tabel',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})',
|
snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const ja: LanguageTranslation = {
|
|||||||
new: '新規',
|
new: '新規',
|
||||||
open: '開く',
|
open: '開く',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
import_database: 'データベースをインポート',
|
import: 'データベースをインポート',
|
||||||
export_sql: 'SQLをエクスポート',
|
export_sql: 'SQLをエクスポート',
|
||||||
export_as: '形式を指定してエクスポート',
|
export_as: '形式を指定してエクスポート',
|
||||||
delete_diagram: 'ダイアグラムを削除',
|
delete_diagram: 'ダイアグラムを削除',
|
||||||
@@ -36,13 +36,14 @@ export const ja: LanguageTranslation = {
|
|||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
backup: {
|
||||||
share: 'Share',
|
backup: 'Backup',
|
||||||
export_diagram: 'Export Diagram',
|
export_diagram: 'Export Diagram',
|
||||||
import_diagram: 'Import Diagram',
|
restore_diagram: 'Restore Diagram',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'ヘルプ',
|
help: 'ヘルプ',
|
||||||
|
docs_website: 'ドキュメント',
|
||||||
visit_website: 'ChartDBにアクセス',
|
visit_website: 'ChartDBにアクセス',
|
||||||
join_discord: 'Discordに参加',
|
join_discord: 'Discordに参加',
|
||||||
schedule_a_call: '話しかけてください!',
|
schedule_a_call: '話しかけてください!',
|
||||||
@@ -128,6 +129,12 @@ export const ja: LanguageTranslation = {
|
|||||||
add_table: 'テーブルを追加',
|
add_table: 'テーブルを追加',
|
||||||
filter: 'フィルタ',
|
filter: 'フィルタ',
|
||||||
collapse: 'すべて折りたたむ',
|
collapse: 'すべて折りたたむ',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'フィールド',
|
fields: 'フィールド',
|
||||||
@@ -148,6 +155,8 @@ export const ja: LanguageTranslation = {
|
|||||||
comments: 'コメント',
|
comments: 'コメント',
|
||||||
no_comments: 'コメントがありません',
|
no_comments: 'コメントがありません',
|
||||||
delete_field: 'フィールドを削除',
|
delete_field: 'フィールドを削除',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'インデックス属性',
|
title: 'インデックス属性',
|
||||||
@@ -380,6 +389,20 @@ export const ja: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: '1対1',
|
one_to_one: '1対1',
|
||||||
one_to_many: '1対多',
|
one_to_many: '1対多',
|
||||||
@@ -396,6 +419,7 @@ export const ja: LanguageTranslation = {
|
|||||||
edit_table: 'テーブルを編集',
|
edit_table: 'テーブルを編集',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
delete_table: 'テーブルを削除',
|
delete_table: 'テーブルを削除',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Add translations
|
// TODO: Add translations
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
new: '새 다이어그램',
|
new: '새 다이어그램',
|
||||||
open: '열기',
|
open: '열기',
|
||||||
save: '저장',
|
save: '저장',
|
||||||
import_database: '데이터베이스 가져오기',
|
import: '데이터베이스 가져오기',
|
||||||
export_sql: 'SQL로 저장',
|
export_sql: 'SQL로 저장',
|
||||||
export_as: '다른 형식으로 저장',
|
export_as: '다른 형식으로 저장',
|
||||||
delete_diagram: '다이어그램 삭제',
|
delete_diagram: '다이어그램 삭제',
|
||||||
@@ -34,13 +34,14 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
backup: {
|
||||||
share: '공유',
|
backup: '백업',
|
||||||
export_diagram: '다이어그램 내보내기',
|
export_diagram: '다이어그램 내보내기',
|
||||||
import_diagram: '다이어그램 가져오기',
|
restore_diagram: '다이어그램 복구',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: '도움말',
|
help: '도움말',
|
||||||
|
docs_website: '선적 서류 비치',
|
||||||
visit_website: 'ChartDB 사이트 방문',
|
visit_website: 'ChartDB 사이트 방문',
|
||||||
join_discord: 'Discord 가입',
|
join_discord: 'Discord 가입',
|
||||||
schedule_a_call: 'Talk with us!',
|
schedule_a_call: 'Talk with us!',
|
||||||
@@ -124,6 +125,12 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
add_table: '테이블 추가',
|
add_table: '테이블 추가',
|
||||||
filter: '필터',
|
filter: '필터',
|
||||||
collapse: '모두 접기',
|
collapse: '모두 접기',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: '필드',
|
fields: '필드',
|
||||||
@@ -144,6 +151,8 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
comments: '주석',
|
comments: '주석',
|
||||||
no_comments: '주석 없음',
|
no_comments: '주석 없음',
|
||||||
delete_field: '필드 삭제',
|
delete_field: '필드 삭제',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: '인덱스 속성',
|
title: '인덱스 속성',
|
||||||
@@ -369,6 +378,20 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
|
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: '일대일 (1:1)',
|
one_to_one: '일대일 (1:1)',
|
||||||
one_to_many: '일대다 (1:N)',
|
one_to_many: '일대다 (1:N)',
|
||||||
@@ -385,6 +408,7 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
edit_table: '테이블 수정',
|
edit_table: '테이블 수정',
|
||||||
duplicate_table: '테이블 복제',
|
duplicate_table: '테이블 복제',
|
||||||
delete_table: '테이블 삭제',
|
delete_table: '테이블 삭제',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)',
|
snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const mr: LanguageTranslation = {
|
|||||||
new: 'नवीन',
|
new: 'नवीन',
|
||||||
open: 'उघडा',
|
open: 'उघडा',
|
||||||
save: 'जतन करा',
|
save: 'जतन करा',
|
||||||
import_database: 'डेटाबेस इम्पोर्ट करा',
|
import: 'डेटाबेस इम्पोर्ट करा',
|
||||||
export_sql: 'SQL एक्स्पोर्ट करा',
|
export_sql: 'SQL एक्स्पोर्ट करा',
|
||||||
export_as: 'म्हणून एक्स्पोर्ट करा',
|
export_as: 'म्हणून एक्स्पोर्ट करा',
|
||||||
delete_diagram: 'आरेख हटवा',
|
delete_diagram: 'आरेख हटवा',
|
||||||
@@ -34,14 +34,15 @@ export const mr: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
backup: {
|
||||||
// TODO: Add translations
|
// TODO: Add translations
|
||||||
share: 'Share',
|
backup: 'Backup',
|
||||||
export_diagram: 'Export Diagram',
|
export_diagram: 'Export Diagram',
|
||||||
import_diagram: 'Import Diagram',
|
restore_diagram: 'Restore Diagram',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'मदत',
|
help: 'मदत',
|
||||||
|
docs_website: 'दस्तऐवजीकरण',
|
||||||
visit_website: 'ChartDB ला भेट द्या',
|
visit_website: 'ChartDB ला भेट द्या',
|
||||||
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
|
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
|
||||||
schedule_a_call: 'आमच्याशी बोला!',
|
schedule_a_call: 'आमच्याशी बोला!',
|
||||||
@@ -127,6 +128,12 @@ export const mr: LanguageTranslation = {
|
|||||||
add_table: 'टेबल जोडा',
|
add_table: 'टेबल जोडा',
|
||||||
filter: 'फिल्टर',
|
filter: 'फिल्टर',
|
||||||
collapse: 'सर्व संकुचित करा',
|
collapse: 'सर्व संकुचित करा',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'फील्ड्स',
|
fields: 'फील्ड्स',
|
||||||
@@ -147,6 +154,8 @@ export const mr: LanguageTranslation = {
|
|||||||
comments: 'टिप्पण्या',
|
comments: 'टिप्पण्या',
|
||||||
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
||||||
delete_field: 'फील्ड हटवा',
|
delete_field: 'फील्ड हटवा',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'इंडेक्स गुणधर्म',
|
title: 'इंडेक्स गुणधर्म',
|
||||||
@@ -381,6 +390,20 @@ export const mr: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'एक ते एक',
|
one_to_one: 'एक ते एक',
|
||||||
@@ -397,8 +420,8 @@ export const mr: LanguageTranslation = {
|
|||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
edit_table: 'टेबल संपादित करा',
|
edit_table: 'टेबल संपादित करा',
|
||||||
delete_table: 'टेबल हटवा',
|
delete_table: 'टेबल हटवा',
|
||||||
// TODO: Add translations
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
duplicate_table: 'Duplicate Table',
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Add translations
|
// TODO: Add translations
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const ne: LanguageTranslation = {
|
|||||||
new: 'नयाँ',
|
new: 'नयाँ',
|
||||||
open: 'खोल्नुहोस्',
|
open: 'खोल्नुहोस्',
|
||||||
save: 'सुरक्षित गर्नुहोस्',
|
save: 'सुरक्षित गर्नुहोस्',
|
||||||
import_database: 'डाटाबेस आयात गर्नुहोस्',
|
import: 'डाटाबेस आयात गर्नुहोस्',
|
||||||
export_sql: 'SQL निर्यात गर्नुहोस्',
|
export_sql: 'SQL निर्यात गर्नुहोस्',
|
||||||
export_as: 'निर्यात गर्नुहोस्',
|
export_as: 'निर्यात गर्नुहोस्',
|
||||||
delete_diagram: 'डायाग्राम हटाउनुहोस्',
|
delete_diagram: 'डायाग्राम हटाउनुहोस्',
|
||||||
@@ -34,13 +34,15 @@ export const ne: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
// TODO: Translate
|
||||||
share: 'शेयर गर्नुहोस्',
|
backup: {
|
||||||
export_diagram: 'डायाग्राम निर्यात गर्नुहोस्',
|
backup: 'Backup',
|
||||||
import_diagram: 'डायाग्राम आयात गर्नुहोस्',
|
export_diagram: 'Export Diagram',
|
||||||
|
restore_diagram: 'Restore Diagram',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'मद्दत',
|
help: 'मद्दत',
|
||||||
|
docs_website: 'कागजात',
|
||||||
visit_website: 'वेबसाइटमा जानुहोस्',
|
visit_website: 'वेबसाइटमा जानुहोस्',
|
||||||
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
|
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
|
||||||
schedule_a_call: 'कल अनुसूची गर्नुहोस्',
|
schedule_a_call: 'कल अनुसूची गर्नुहोस्',
|
||||||
@@ -124,6 +126,12 @@ export const ne: LanguageTranslation = {
|
|||||||
add_table: 'तालिका थप्नुहोस्',
|
add_table: 'तालिका थप्नुहोस्',
|
||||||
filter: 'फिल्टर',
|
filter: 'फिल्टर',
|
||||||
collapse: 'सबै लुकाउनुहोस्',
|
collapse: 'सबै लुकाउनुहोस्',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'क्षेत्रहरू',
|
fields: 'क्षेत्रहरू',
|
||||||
@@ -144,6 +152,8 @@ export const ne: LanguageTranslation = {
|
|||||||
comments: 'टिप्पणीहरू',
|
comments: 'टिप्पणीहरू',
|
||||||
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
||||||
delete_field: 'क्षेत्र हटाउनुहोस्',
|
delete_field: 'क्षेत्र हटाउनुहोस्',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'सूचक विशेषताहरू',
|
title: 'सूचक विशेषताहरू',
|
||||||
@@ -374,6 +384,20 @@ export const ne: LanguageTranslation = {
|
|||||||
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
|
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'एक देखि एक',
|
one_to_one: 'एक देखि एक',
|
||||||
@@ -391,6 +415,7 @@ export const ne: LanguageTranslation = {
|
|||||||
edit_table: 'तालिका सम्पादन गर्नुहोस्',
|
edit_table: 'तालिका सम्पादन गर्नुहोस्',
|
||||||
duplicate_table: 'तालिका नक्कली गर्नुहोस्',
|
duplicate_table: 'तालिका नक्कली गर्नुहोस्',
|
||||||
delete_table: 'तालिका हटाउनुहोस्',
|
delete_table: 'तालिका हटाउनुहोस्',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)',
|
snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
new: 'Novo',
|
new: 'Novo',
|
||||||
open: 'Abrir',
|
open: 'Abrir',
|
||||||
save: 'Salvar',
|
save: 'Salvar',
|
||||||
import_database: 'Importar Banco de Dados',
|
import: 'Importar Banco de Dados',
|
||||||
export_sql: 'Exportar SQL',
|
export_sql: 'Exportar SQL',
|
||||||
export_as: 'Exportar como',
|
export_as: 'Exportar como',
|
||||||
delete_diagram: 'Excluir Diagrama',
|
delete_diagram: 'Excluir Diagrama',
|
||||||
@@ -35,13 +35,14 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
backup: {
|
||||||
share: 'Share',
|
backup: 'Backup',
|
||||||
export_diagram: 'Export Diagram',
|
export_diagram: 'Exportar Diagrama',
|
||||||
import_diagram: 'Import Diagram',
|
restore_diagram: 'Restaurar Diagrama',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Ajuda',
|
help: 'Ajuda',
|
||||||
|
docs_website: 'Documentação',
|
||||||
visit_website: 'Visitar ChartDB',
|
visit_website: 'Visitar ChartDB',
|
||||||
join_discord: 'Junte-se a nós no Discord',
|
join_discord: 'Junte-se a nós no Discord',
|
||||||
schedule_a_call: 'Fale Conosco!',
|
schedule_a_call: 'Fale Conosco!',
|
||||||
@@ -125,6 +126,12 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
add_table: 'Adicionar Tabela',
|
add_table: 'Adicionar Tabela',
|
||||||
filter: 'Filtrar',
|
filter: 'Filtrar',
|
||||||
collapse: 'Colapsar Todas',
|
collapse: 'Colapsar Todas',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Campos',
|
fields: 'Campos',
|
||||||
@@ -145,6 +152,8 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
comments: 'Comentários',
|
comments: 'Comentários',
|
||||||
no_comments: 'Sem comentários',
|
no_comments: 'Sem comentários',
|
||||||
delete_field: 'Excluir Campo',
|
delete_field: 'Excluir Campo',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Atributos do Índice',
|
title: 'Atributos do Índice',
|
||||||
@@ -374,6 +383,20 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Um para Um',
|
one_to_one: 'Um para Um',
|
||||||
one_to_many: 'Um para Muitos',
|
one_to_many: 'Um para Muitos',
|
||||||
@@ -390,6 +413,7 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
edit_table: 'Editar Tabela',
|
edit_table: 'Editar Tabela',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
delete_table: 'Excluir Tabela',
|
delete_table: 'Excluir Tabela',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Add translations
|
// TODO: Add translations
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const ru: LanguageTranslation = {
|
|||||||
new: 'Создать',
|
new: 'Создать',
|
||||||
open: 'Открыть',
|
open: 'Открыть',
|
||||||
save: 'Сохранить',
|
save: 'Сохранить',
|
||||||
import_database: 'Импортировать базу данных',
|
import: 'Импортировать базу данных',
|
||||||
export_sql: 'Экспорт SQL',
|
export_sql: 'Экспорт SQL',
|
||||||
export_as: 'Экспортировать как',
|
export_as: 'Экспортировать как',
|
||||||
delete_diagram: 'Удалить диаграмму',
|
delete_diagram: 'Удалить диаграмму',
|
||||||
@@ -34,13 +34,15 @@ export const ru: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
// TODO: Translate
|
||||||
share: 'Поделиться',
|
backup: {
|
||||||
export_diagram: 'Экспорт кода диаграммы',
|
backup: 'Backup',
|
||||||
import_diagram: 'Импорт кода диаграммы',
|
export_diagram: 'Export Diagram',
|
||||||
|
restore_diagram: 'Restore Diagram',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Помощь',
|
help: 'Помощь',
|
||||||
|
docs_website: 'Документация',
|
||||||
visit_website: 'Перейти на сайт ChartDB',
|
visit_website: 'Перейти на сайт ChartDB',
|
||||||
join_discord: 'Присоединиться к сообществу в Discord',
|
join_discord: 'Присоединиться к сообществу в Discord',
|
||||||
schedule_a_call: 'Поговорите с нами!',
|
schedule_a_call: 'Поговорите с нами!',
|
||||||
@@ -123,6 +125,12 @@ export const ru: LanguageTranslation = {
|
|||||||
add_table: 'Добавить таблицу',
|
add_table: 'Добавить таблицу',
|
||||||
filter: 'Фильтр',
|
filter: 'Фильтр',
|
||||||
collapse: 'Свернуть все',
|
collapse: 'Свернуть все',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Поля',
|
fields: 'Поля',
|
||||||
@@ -143,6 +151,8 @@ export const ru: LanguageTranslation = {
|
|||||||
comments: 'Комментарии',
|
comments: 'Комментарии',
|
||||||
no_comments: 'Нет комментария',
|
no_comments: 'Нет комментария',
|
||||||
delete_field: 'Удалить поле',
|
delete_field: 'Удалить поле',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Атрибуты индекса',
|
title: 'Атрибуты индекса',
|
||||||
@@ -370,6 +380,20 @@ export const ru: LanguageTranslation = {
|
|||||||
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
|
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Один к одному',
|
one_to_one: 'Один к одному',
|
||||||
one_to_many: 'Один ко многим',
|
one_to_many: 'Один ко многим',
|
||||||
@@ -386,6 +410,7 @@ export const ru: LanguageTranslation = {
|
|||||||
edit_table: 'Изменить таблицу',
|
edit_table: 'Изменить таблицу',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
delete_table: 'Удалить таблицу',
|
delete_table: 'Удалить таблицу',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
copy_to_clipboard: 'Скопировать в буфер обмена',
|
copy_to_clipboard: 'Скопировать в буфер обмена',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const te: LanguageTranslation = {
|
|||||||
new: 'కొత్తది',
|
new: 'కొత్తది',
|
||||||
open: 'తెరవు',
|
open: 'తెరవు',
|
||||||
save: 'సేవ్',
|
save: 'సేవ్',
|
||||||
import_database: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
|
import: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
|
||||||
export_sql: 'SQL ఎగుమతి',
|
export_sql: 'SQL ఎగుమతి',
|
||||||
export_as: 'వగా ఎగుమతి చేయండి',
|
export_as: 'వగా ఎగుమతి చేయండి',
|
||||||
delete_diagram: 'చిత్రాన్ని తొలగించండి',
|
delete_diagram: 'చిత్రాన్ని తొలగించండి',
|
||||||
@@ -35,13 +35,14 @@ export const te: LanguageTranslation = {
|
|||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
backup: {
|
||||||
share: 'Share',
|
backup: 'Backup',
|
||||||
export_diagram: 'Export Diagram',
|
export_diagram: 'Export Diagram',
|
||||||
import_diagram: 'Import Diagram',
|
restore_diagram: 'Restore Diagram',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'సహాయం',
|
help: 'సహాయం',
|
||||||
|
docs_website: 'డాక్యుమెంటేషన్',
|
||||||
visit_website: 'ChartDB సందర్శించండి',
|
visit_website: 'ChartDB సందర్శించండి',
|
||||||
join_discord: 'డిస్కార్డ్లో మా నుంచి చేరండి',
|
join_discord: 'డిస్కార్డ్లో మా నుంచి చేరండి',
|
||||||
schedule_a_call: 'మాతో మాట్లాడండి!',
|
schedule_a_call: 'మాతో మాట్లాడండి!',
|
||||||
@@ -125,6 +126,12 @@ export const te: LanguageTranslation = {
|
|||||||
add_table: 'పట్టికను జోడించు',
|
add_table: 'పట్టికను జోడించు',
|
||||||
filter: 'ఫిల్టర్',
|
filter: 'ఫిల్టర్',
|
||||||
collapse: 'అన్ని కూల్ చేయి',
|
collapse: 'అన్ని కూల్ చేయి',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'ఫీల్డులు',
|
fields: 'ఫీల్డులు',
|
||||||
@@ -145,6 +152,8 @@ export const te: LanguageTranslation = {
|
|||||||
comments: 'వ్యాఖ్యలు',
|
comments: 'వ్యాఖ్యలు',
|
||||||
no_comments: 'వ్యాఖ్యలు లేవు',
|
no_comments: 'వ్యాఖ్యలు లేవు',
|
||||||
delete_field: 'ఫీల్డ్ తొలగించు',
|
delete_field: 'ఫీల్డ్ తొలగించు',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'ఇండెక్స్ గుణాలు',
|
title: 'ఇండెక్స్ గుణాలు',
|
||||||
@@ -377,6 +386,20 @@ export const te: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'ఒకటి_కీ_ఒకటి',
|
one_to_one: 'ఒకటి_కీ_ఒకటి',
|
||||||
@@ -392,9 +415,9 @@ export const te: LanguageTranslation = {
|
|||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
edit_table: 'పట్టికను సవరించు',
|
edit_table: 'పట్టికను సవరించు',
|
||||||
// TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
duplicate_table: 'Duplicate Table',
|
|
||||||
delete_table: 'పట్టికను తొలగించు',
|
delete_table: 'పట్టికను తొలగించు',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const tr: LanguageTranslation = {
|
|||||||
new: 'Yeni',
|
new: 'Yeni',
|
||||||
open: 'Aç',
|
open: 'Aç',
|
||||||
save: 'Kaydet',
|
save: 'Kaydet',
|
||||||
import_database: 'Veritabanı İçe Aktar',
|
import: 'Veritabanı İçe Aktar',
|
||||||
export_sql: 'SQL Olarak Dışa Aktar',
|
export_sql: 'SQL Olarak Dışa Aktar',
|
||||||
export_as: 'Olarak Dışa Aktar',
|
export_as: 'Olarak Dışa Aktar',
|
||||||
delete_diagram: 'Diyagramı Sil',
|
delete_diagram: 'Diyagramı Sil',
|
||||||
@@ -35,13 +35,14 @@ export const tr: LanguageTranslation = {
|
|||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
backup: {
|
||||||
share: 'Share',
|
backup: 'Backup',
|
||||||
export_diagram: 'Export Diagram',
|
export_diagram: 'Export Diagram',
|
||||||
import_diagram: 'Import Diagram',
|
restore_diagram: 'Restore Diagram',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Yardım',
|
help: 'Yardım',
|
||||||
|
docs_website: 'Belgeleme',
|
||||||
visit_website: "ChartDB'yi Ziyaret Et",
|
visit_website: "ChartDB'yi Ziyaret Et",
|
||||||
join_discord: "Discord'a Katıl",
|
join_discord: "Discord'a Katıl",
|
||||||
schedule_a_call: 'Bize Ulaş!',
|
schedule_a_call: 'Bize Ulaş!',
|
||||||
@@ -124,6 +125,13 @@ export const tr: LanguageTranslation = {
|
|||||||
add_table: 'Tablo Ekle',
|
add_table: 'Tablo Ekle',
|
||||||
filter: 'Filtrele',
|
filter: 'Filtrele',
|
||||||
collapse: 'Hepsini Daralt',
|
collapse: 'Hepsini Daralt',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Alanlar',
|
fields: 'Alanlar',
|
||||||
nullable: 'Boş Bırakılabilir?',
|
nullable: 'Boş Bırakılabilir?',
|
||||||
@@ -143,6 +151,8 @@ export const tr: LanguageTranslation = {
|
|||||||
comments: 'Yorumlar',
|
comments: 'Yorumlar',
|
||||||
no_comments: 'Yorum yok',
|
no_comments: 'Yorum yok',
|
||||||
delete_field: 'Alanı Sil',
|
delete_field: 'Alanı Sil',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'İndeks Özellikleri',
|
title: 'İndeks Özellikleri',
|
||||||
@@ -363,6 +373,20 @@ export const tr: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Bir Bir',
|
one_to_one: 'Bir Bir',
|
||||||
one_to_many: 'Bir Çok',
|
one_to_many: 'Bir Çok',
|
||||||
@@ -376,8 +400,8 @@ export const tr: LanguageTranslation = {
|
|||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
edit_table: 'Tabloyu Düzenle',
|
edit_table: 'Tabloyu Düzenle',
|
||||||
delete_table: 'Tabloyu Sil',
|
delete_table: 'Tabloyu Sil',
|
||||||
// TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
duplicate_table: 'Duplicate Table',
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
|||||||
@@ -4,47 +4,46 @@ export const uk: LanguageTranslation = {
|
|||||||
translation: {
|
translation: {
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
file: {
|
||||||
file: 'файл',
|
file: 'Файл',
|
||||||
new: 'новий',
|
new: 'Новий',
|
||||||
open: 'відкрити',
|
open: 'Відкрити',
|
||||||
save: 'зберегти',
|
save: 'Зберегти',
|
||||||
import_database: 'Імпорт бази даних',
|
import: 'Імпорт бази даних',
|
||||||
export_sql: 'Експорт SQL',
|
export_sql: 'Експорт SQL',
|
||||||
export_as: 'Експортувати як',
|
export_as: 'Експортувати як',
|
||||||
delete_diagram: 'Видалити діаграму',
|
delete_diagram: 'Видалити діаграму',
|
||||||
exit: 'вийти',
|
exit: 'Вийти',
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'редагувати',
|
edit: 'Редагувати',
|
||||||
undo: 'Скасувати',
|
undo: 'Скасувати',
|
||||||
redo: 'Повторити',
|
redo: 'Повторити',
|
||||||
clear: 'очистити',
|
clear: 'Очистити',
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
view: 'переглянути',
|
view: 'Перегляд',
|
||||||
show_sidebar: 'Показати бічну панель',
|
show_sidebar: 'Показати бічну панель',
|
||||||
hide_sidebar: 'Приховати бічну панель',
|
hide_sidebar: 'Приховати бічну панель',
|
||||||
hide_cardinality: 'Приховати потужність',
|
hide_cardinality: 'Приховати потужність',
|
||||||
show_cardinality: 'Показати кардинальність',
|
show_cardinality: 'Показати кардинальність',
|
||||||
zoom_on_scroll: 'Збільшити прокручування',
|
zoom_on_scroll: 'Масштабувати прокручуванням',
|
||||||
theme: 'Тема',
|
theme: 'Тема',
|
||||||
show_dependencies: 'Показати залежності',
|
show_dependencies: 'Показати залежності',
|
||||||
hide_dependencies: 'Приховати залежності',
|
hide_dependencies: 'Приховати залежності',
|
||||||
// TODO: Translate
|
show_minimap: 'Показати мінімапу',
|
||||||
show_minimap: 'Show Mini Map',
|
hide_minimap: 'Приховати мінімапу',
|
||||||
hide_minimap: 'Hide Mini Map',
|
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
backup: {
|
||||||
share: {
|
backup: 'Резервне копіювання',
|
||||||
share: 'Share',
|
export_diagram: 'Експорт діаграми',
|
||||||
export_diagram: 'Export Diagram',
|
restore_diagram: 'Відновити діаграму',
|
||||||
import_diagram: 'Import Diagram',
|
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Допомога',
|
help: 'Довідка',
|
||||||
visit_website: 'Відвідайте ChartDB',
|
docs_website: 'Документація',
|
||||||
|
visit_website: 'Сайт ChartDB',
|
||||||
join_discord: 'Приєднуйтесь до нас в Діскорд',
|
join_discord: 'Приєднуйтесь до нас в Діскорд',
|
||||||
schedule_a_call: 'Поговоріть з нами!',
|
schedule_a_call: 'Забронювати зустріч!',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -57,18 +56,18 @@ export const uk: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
clear_diagram_alert: {
|
clear_diagram_alert: {
|
||||||
title: 'Чітка діаграма',
|
title: 'Очистити діаграму',
|
||||||
description:
|
description:
|
||||||
'Цю дію не можна скасувати. Це назавжди видалить усі дані на діаграмі.',
|
'Цю дію не можна скасувати. Це назавжди видалить усі дані на діаграмі.',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
clear: 'очистити',
|
clear: 'Очистити',
|
||||||
},
|
},
|
||||||
|
|
||||||
reorder_diagram_alert: {
|
reorder_diagram_alert: {
|
||||||
title: 'Діаграма зміни порядку',
|
title: 'Перевпорядкувати діаграму',
|
||||||
description:
|
description:
|
||||||
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
|
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
|
||||||
reorder: 'Змінити порядок',
|
reorder: 'Перевпорядкувати',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -93,23 +92,23 @@ export const uk: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
system: 'система',
|
system: 'Системна',
|
||||||
light: 'світлий',
|
light: 'Світла',
|
||||||
dark: 'Темний',
|
dark: 'Темна',
|
||||||
},
|
},
|
||||||
|
|
||||||
zoom: {
|
zoom: {
|
||||||
on: 'увімкнути',
|
on: 'Увімкнути',
|
||||||
off: 'вимкнути',
|
off: 'Вимкнути',
|
||||||
},
|
},
|
||||||
|
|
||||||
last_saved: 'Востаннє збережено',
|
last_saved: 'Востаннє збережено',
|
||||||
saved: 'Збережено',
|
saved: 'Збережено',
|
||||||
loading_diagram: 'Діаграма завантаження...',
|
loading_diagram: 'Завантаження діаграми…',
|
||||||
deselect_all: 'Зняти вибір із усіх',
|
deselect_all: 'Зняти виділення з усіх',
|
||||||
select_all: 'Вибрати усі',
|
select_all: 'Вибрати усі',
|
||||||
clear: 'Очистити',
|
clear: 'Очистити',
|
||||||
show_more: 'показати більше',
|
show_more: 'Показати більше',
|
||||||
show_less: 'Показати менше',
|
show_less: 'Показати менше',
|
||||||
copy_to_clipboard: 'Копіювати в буфер обміну',
|
copy_to_clipboard: 'Копіювати в буфер обміну',
|
||||||
copied: 'Скопійовано!',
|
copied: 'Скопійовано!',
|
||||||
@@ -117,47 +116,55 @@ export const uk: LanguageTranslation = {
|
|||||||
side_panel: {
|
side_panel: {
|
||||||
schema: 'Схема:',
|
schema: 'Схема:',
|
||||||
filter_by_schema: 'Фільтрувати за схемою',
|
filter_by_schema: 'Фільтрувати за схемою',
|
||||||
search_schema: 'Схема пошуку...',
|
search_schema: 'Пошук схеми…',
|
||||||
no_schemas_found: 'Схеми не знайдено.',
|
no_schemas_found: 'Схеми не знайдено.',
|
||||||
view_all_options: 'Переглянути всі параметри...',
|
view_all_options: 'Переглянути всі параметри…',
|
||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Таблиці',
|
tables: 'Таблиці',
|
||||||
add_table: 'Додати таблицю',
|
add_table: 'Додати таблицю',
|
||||||
filter: 'фільтр',
|
filter: 'Фільтр',
|
||||||
collapse: 'Згорнути все',
|
collapse: 'Згорнути все',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'поля',
|
fields: 'Поля',
|
||||||
nullable: 'Зведений нанівець?',
|
nullable: 'Може бути Null?',
|
||||||
primary_key: 'Первинний ключ',
|
primary_key: 'Первинний ключ',
|
||||||
indexes: 'Індекси',
|
indexes: 'Індекси',
|
||||||
comments: 'Коментарі',
|
comments: 'Коментарі',
|
||||||
no_comments: 'Без коментарів',
|
no_comments: 'Немає коментарів',
|
||||||
add_field: 'Додати поле',
|
add_field: 'Додати поле',
|
||||||
add_index: 'Додати індекс',
|
add_index: 'Додати індекс',
|
||||||
index_select_fields: 'Виберіть поля',
|
index_select_fields: 'Виберіть поля',
|
||||||
no_types_found: 'Типи не знайдено',
|
no_types_found: 'Типи не знайдено',
|
||||||
field_name: "Ім'я",
|
field_name: 'Назва поля',
|
||||||
field_type: 'Тип',
|
field_type: 'Тип',
|
||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Атрибути полів',
|
title: 'Атрибути полів',
|
||||||
unique: 'Унікальний',
|
unique: 'Унікальне',
|
||||||
comments: 'Коментарі',
|
comments: 'Коментарі',
|
||||||
no_comments: 'Без коментарів',
|
no_comments: 'Немає коментарів',
|
||||||
delete_field: 'Видалити поле',
|
delete_field: 'Видалити поле',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Атрибути індексу',
|
title: 'Атрибути індексу',
|
||||||
name: "Ім'я",
|
name: 'Назва індекса',
|
||||||
unique: 'Унікальний',
|
unique: 'Унікальний',
|
||||||
delete_index: 'Видалити індекс',
|
delete_index: 'Видалити індекс',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
title: 'Дії таблиці',
|
title: 'Дії з таблицею',
|
||||||
change_schema: 'Змінити схему',
|
change_schema: 'Змінити схему',
|
||||||
add_field: 'Додати поле',
|
add_field: 'Додати поле',
|
||||||
add_index: 'Додати індекс',
|
add_index: 'Додати індекс',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Дублювати таблицю',
|
||||||
delete_table: 'Видалити таблицю',
|
delete_table: 'Видалити таблицю',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -167,14 +174,14 @@ export const uk: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
relationships_section: {
|
||||||
relationships: 'стосунки',
|
relationships: 'Звʼязки',
|
||||||
filter: 'фільтр',
|
filter: 'Фільтр',
|
||||||
add_relationship: "Додати зв'язок",
|
add_relationship: 'Додати звʼязок',
|
||||||
collapse: 'Згорнути все',
|
collapse: 'Згорнути все',
|
||||||
relationship: {
|
relationship: {
|
||||||
primary: 'Первинна таблиця',
|
primary: 'Первинна таблиця',
|
||||||
foreign: 'Посилання на таблицю',
|
foreign: 'Посилання на таблицю',
|
||||||
cardinality: 'Кардинальність',
|
cardinality: 'Звʼязок',
|
||||||
delete_relationship: 'Видалити',
|
delete_relationship: 'Видалити',
|
||||||
relationship_actions: {
|
relationship_actions: {
|
||||||
title: 'Дії',
|
title: 'Дії',
|
||||||
@@ -182,17 +189,17 @@ export const uk: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Жодних стосунків',
|
title: 'Звʼязків немає',
|
||||||
description: 'Створіть зв’язок для з’єднання таблиць',
|
description: 'Створіть звʼязок для зʼєднання таблиць',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dependencies_section: {
|
dependencies_section: {
|
||||||
dependencies: 'Залежності',
|
dependencies: 'Залежності',
|
||||||
filter: 'фільтр',
|
filter: 'Фільтр',
|
||||||
collapse: 'Згорнути все',
|
collapse: 'Згорнути все',
|
||||||
dependency: {
|
dependency: {
|
||||||
table: 'Таблиця',
|
table: 'Таблиця',
|
||||||
dependent_table: 'Залежний вид',
|
dependent_table: 'Залежне подання',
|
||||||
delete_dependency: 'Видалити',
|
delete_dependency: 'Видалити',
|
||||||
dependency_actions: {
|
dependency_actions: {
|
||||||
title: 'Дії',
|
title: 'Дії',
|
||||||
@@ -209,34 +216,34 @@ export const uk: LanguageTranslation = {
|
|||||||
toolbar: {
|
toolbar: {
|
||||||
zoom_in: 'Збільшити',
|
zoom_in: 'Збільшити',
|
||||||
zoom_out: 'Зменшити',
|
zoom_out: 'Зменшити',
|
||||||
save: 'зберегти',
|
save: 'Зберегти',
|
||||||
show_all: 'Показати все',
|
show_all: 'Показати все',
|
||||||
undo: 'Скасувати',
|
undo: 'Скасувати',
|
||||||
redo: 'Повторити',
|
redo: 'Повторити',
|
||||||
reorder_diagram: 'Діаграма зміни порядку',
|
reorder_diagram: 'Перевпорядкувати діаграму',
|
||||||
highlight_overlapping_tables: 'Виділіть таблиці, що перекриваються',
|
highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
database_selection: {
|
database_selection: {
|
||||||
title: 'Що таке ваша база даних?',
|
title: 'Яка у вас база даних?',
|
||||||
description:
|
description:
|
||||||
'Кожна база даних має свої унікальні особливості та можливості.',
|
'Кожна база даних має свої унікальні особливості та можливості.',
|
||||||
check_examples_long: 'Перевірте приклади',
|
check_examples_long: 'Подивіться приклади',
|
||||||
check_examples_short: 'Приклади',
|
check_examples_short: 'Приклади',
|
||||||
},
|
},
|
||||||
|
|
||||||
import_database: {
|
import_database: {
|
||||||
title: 'Імпортуйте вашу базу даних',
|
title: 'Імпортуйте вашу базу даних',
|
||||||
database_edition: 'Редакція бази даних:',
|
database_edition: 'Варіант бази даних:',
|
||||||
step_1: 'Запустіть цей сценарій у своїй базі даних:',
|
step_1: 'Запустіть цей сценарій у своїй базі даних:',
|
||||||
step_2: 'Вставте сюди результат сценарію:',
|
step_2: 'Вставте сюди результат сценарію:',
|
||||||
script_results_placeholder: 'Результати сценарію тут...',
|
script_results_placeholder: 'Результати сценарію має бути тут…',
|
||||||
ssms_instructions: {
|
ssms_instructions: {
|
||||||
button_text: 'SSMS Інструкції',
|
button_text: 'SSMS Інструкції',
|
||||||
title: 'Інструкції',
|
title: 'Інструкції',
|
||||||
step_1: 'Перейдіть до Інструменти > Опції > Результати запиту > SQL Сервер.',
|
step_1: 'Перейдіть до Інструменти > Опції > Результати запиту > SQL Сервер.',
|
||||||
step_2: 'Якщо ви використовуєте «Результати в сітку», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
|
step_2: 'Якщо ви використовуєте «Results to Grid», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
|
||||||
},
|
},
|
||||||
instructions_link: 'Потрібна допомога? Подивіться як',
|
instructions_link: 'Потрібна допомога? Подивіться як',
|
||||||
check_script_result: 'Перевірте результат сценарію',
|
check_script_result: 'Перевірте результат сценарію',
|
||||||
@@ -244,20 +251,19 @@ export const uk: LanguageTranslation = {
|
|||||||
|
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
back: 'Назад',
|
back: 'Назад',
|
||||||
// TODO: Translate
|
import_from_file: 'Імпортувати з файлу',
|
||||||
import_from_file: 'Import from File',
|
|
||||||
empty_diagram: 'Порожня діаграма',
|
empty_diagram: 'Порожня діаграма',
|
||||||
continue: 'Продовжити',
|
continue: 'Продовжити',
|
||||||
import: 'Імпорт',
|
import: 'Імпорт',
|
||||||
},
|
},
|
||||||
|
|
||||||
open_diagram_dialog: {
|
open_diagram_dialog: {
|
||||||
title: 'Відкрита діаграма',
|
title: 'Відкрити діаграму',
|
||||||
description:
|
description:
|
||||||
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
|
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
|
||||||
table_columns: {
|
table_columns: {
|
||||||
name: "Ім'я",
|
name: 'Назва',
|
||||||
created_at: 'Створено в',
|
created_at: 'Створено0',
|
||||||
last_modified: 'Востаннє змінено',
|
last_modified: 'Востаннє змінено',
|
||||||
tables_count: 'Таблиці',
|
tables_count: 'Таблиці',
|
||||||
},
|
},
|
||||||
@@ -271,23 +277,23 @@ export const uk: LanguageTranslation = {
|
|||||||
'Експортуйте свою схему діаграми в {{databaseType}} сценарій',
|
'Експортуйте свою схему діаграми в {{databaseType}} сценарій',
|
||||||
close: 'Закрити',
|
close: 'Закрити',
|
||||||
loading: {
|
loading: {
|
||||||
text: 'ШІ створює SQL для {{databaseType}}...',
|
text: 'ШІ створює SQL для {{databaseType}}…',
|
||||||
description: 'Це має зайняти до 30 секунд.',
|
description: 'Це має зайняти до 30 секунд.',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
message:
|
message:
|
||||||
"Помилка створення сценарію SQL. Спробуйте пізніше або <0>зв'яжіться з нами</0>.",
|
'Помилка створення сценарію SQL. Спробуйте пізніше або <0>звʼяжіться з нами</0>.',
|
||||||
description:
|
description:
|
||||||
'Не соромтеся використовувати свій OPENAI_TOKEN, дивіться посібник <0>тут</0>.',
|
'Не соромтеся використовувати свій OPENAI_TOKEN, дивіться посібник <0>тут</0>.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
create_relationship_dialog: {
|
create_relationship_dialog: {
|
||||||
title: 'Створити відносини',
|
title: 'Створити звʼязок',
|
||||||
primary_table: 'Первинна таблиця',
|
primary_table: 'Первинна таблиця',
|
||||||
primary_field: 'Первинне поле',
|
primary_field: 'Первинне поле',
|
||||||
referenced_table: 'Посилання на таблицю',
|
referenced_table: 'Звʼязана таблиця',
|
||||||
referenced_field: 'Поле посилання',
|
referenced_field: 'Повʼязане поле',
|
||||||
primary_table_placeholder: 'Виберіть таблицю',
|
primary_table_placeholder: 'Виберіть таблицю',
|
||||||
primary_field_placeholder: 'Виберіть поле',
|
primary_field_placeholder: 'Виберіть поле',
|
||||||
referenced_table_placeholder: 'Виберіть таблицю',
|
referenced_table_placeholder: 'Виберіть таблицю',
|
||||||
@@ -307,12 +313,12 @@ export const uk: LanguageTranslation = {
|
|||||||
new_tables:
|
new_tables:
|
||||||
'<bold>{{newTablesNumber}}</bold> будуть додані нові таблиці.',
|
'<bold>{{newTablesNumber}}</bold> будуть додані нові таблиці.',
|
||||||
new_relationships:
|
new_relationships:
|
||||||
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові відносини.',
|
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові звʼязки.',
|
||||||
tables_override:
|
tables_override:
|
||||||
'<bold>{{tablesOverrideNumber}}</bold> таблиці будуть перезаписані.',
|
'<bold>{{tablesOverrideNumber}}</bold> таблиці будуть перезаписані.',
|
||||||
proceed: 'Ви хочете продовжити?',
|
proceed: 'Ви хочете продовжити?',
|
||||||
},
|
},
|
||||||
import: 'Імпорт',
|
import: 'Імпортувати',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -320,83 +326,95 @@ export const uk: LanguageTranslation = {
|
|||||||
export_image_dialog: {
|
export_image_dialog: {
|
||||||
title: 'Експорт зображення',
|
title: 'Експорт зображення',
|
||||||
description: 'Виберіть коефіцієнт масштабування для експорту:',
|
description: 'Виберіть коефіцієнт масштабування для експорту:',
|
||||||
scale_1x: '1x Регулярний',
|
scale_1x: '1x Звичайний',
|
||||||
scale_2x: '2x (Рекомендовано)',
|
scale_2x: '2x (Рекомендовано)',
|
||||||
scale_3x: '3x',
|
scale_3x: '3x',
|
||||||
scale_4x: '4x',
|
scale_4x: '4x',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
export: 'Експорт',
|
export: 'Експортувати',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_table_schema_dialog: {
|
new_table_schema_dialog: {
|
||||||
title: 'Виберіть Схему',
|
title: 'Виберіть Схему',
|
||||||
description:
|
description:
|
||||||
'Наразі відображається кілька схем. Виберіть один для нової таблиці.',
|
'Наразі показується кілька схем. Виберіть одну для нової таблиці.',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
confirm: 'Підтвердити',
|
confirm: 'Підтвердити',
|
||||||
},
|
},
|
||||||
|
|
||||||
update_table_schema_dialog: {
|
update_table_schema_dialog: {
|
||||||
title: 'Змінити схему',
|
title: 'Змінити схему',
|
||||||
description: 'Оновити таблицю "{{tableName}}" схему',
|
description: 'Оновити схему таблиці "{{tableName}}"',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
confirm: 'Змінити',
|
confirm: 'Змінити',
|
||||||
},
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Допоможіть нам покращитися!',
|
title: 'Допоможіть нам покращитися!',
|
||||||
description: 'Хочете позначити нас на Ґітхаб? Це лише один клік!',
|
description: 'Поставне на зірку на GitHub? Це лише один клік!',
|
||||||
close: 'Не зараз',
|
close: 'Не зараз',
|
||||||
confirm: 'звичайно!',
|
confirm: 'Звісно!',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
|
||||||
export_diagram_dialog: {
|
export_diagram_dialog: {
|
||||||
title: 'Export Diagram',
|
title: 'Експорт Діаграми',
|
||||||
description: 'Choose the format for export:',
|
description: 'Оберіть формат експорту:',
|
||||||
format_json: 'JSON',
|
format_json: 'JSON',
|
||||||
cancel: 'Cancel',
|
cancel: 'Скасувати',
|
||||||
export: 'Export',
|
export: 'Експортувати',
|
||||||
error: {
|
error: {
|
||||||
title: 'Error exporting diagram',
|
title: 'Помилка експорут діаграми',
|
||||||
description:
|
description:
|
||||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
'Щось пішло не так. Потрібна допомога? chartdb.io@gmail.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
import_diagram_dialog: {
|
||||||
|
title: 'Імпорт Діаграми',
|
||||||
|
description: 'Вставте JSON діаграми нижче:',
|
||||||
|
cancel: 'Скасувати',
|
||||||
|
import: 'Імпортувати',
|
||||||
|
error: {
|
||||||
|
title: 'Помилка імпорту діаграми',
|
||||||
|
description:
|
||||||
|
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
import_diagram_dialog: {
|
import_dbml_dialog: {
|
||||||
title: 'Import Diagram',
|
example_title: 'Import Example DBML',
|
||||||
description: 'Paste the diagram JSON below:',
|
title: 'Import DBML',
|
||||||
cancel: 'Cancel',
|
description: 'Import a database schema from DBML format.',
|
||||||
import: 'Import',
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
error: {
|
error: {
|
||||||
title: 'Error importing diagram',
|
title: 'Error',
|
||||||
description:
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Один до одного',
|
one_to_one: 'Один до Одного',
|
||||||
one_to_many: 'Один до багатьох',
|
one_to_many: 'Один до Багатьох',
|
||||||
many_to_one: 'Багато до одного',
|
many_to_one: 'Багато до Одного',
|
||||||
many_to_many: 'Багато до багатьох',
|
many_to_many: 'Багато до Багатьох',
|
||||||
},
|
},
|
||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Нова таблиця',
|
new_table: 'Нова таблиця',
|
||||||
new_relationship: 'Нові стосунки',
|
new_relationship: 'Новий звʼязок',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
edit_table: 'Редагувати таблицю',
|
edit_table: 'Редагувати таблицю',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Дублювати таблицю',
|
||||||
delete_table: 'Видалити таблицю',
|
delete_table: 'Видалити таблицю',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Add translations
|
snap_to_grid_tooltip: 'Вирівнювати за сіткою (Отримуйте {{key}})',
|
||||||
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
|
||||||
|
|
||||||
tool_tips: {
|
tool_tips: {
|
||||||
double_click_to_edit: 'Двойной клик для редактирования',
|
double_click_to_edit: 'Подвійне клацання для редагування',
|
||||||
},
|
},
|
||||||
|
|
||||||
language_select: {
|
language_select: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const vi: LanguageTranslation = {
|
|||||||
new: 'Tạo mới',
|
new: 'Tạo mới',
|
||||||
open: 'Mở',
|
open: 'Mở',
|
||||||
save: 'Lưu',
|
save: 'Lưu',
|
||||||
import_database: 'Nhập cơ sở dữ liệu',
|
import: 'Nhập cơ sở dữ liệu',
|
||||||
export_sql: 'Xuất SQL',
|
export_sql: 'Xuất SQL',
|
||||||
export_as: 'Xuất thành',
|
export_as: 'Xuất thành',
|
||||||
delete_diagram: 'Xóa sơ đồ',
|
delete_diagram: 'Xóa sơ đồ',
|
||||||
@@ -34,13 +34,14 @@ export const vi: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
backup: {
|
||||||
share: 'Chia sẻ',
|
backup: 'Hỗ trợ',
|
||||||
export_diagram: 'Xuất sơ đồ',
|
export_diagram: 'Xuất sơ đồ',
|
||||||
import_diagram: 'Nhập sơ đồ',
|
restore_diagram: 'Khôi phục sơ đồ',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Trợ giúp',
|
help: 'Trợ giúp',
|
||||||
|
docs_website: 'Tài liệu',
|
||||||
visit_website: 'Truy cập ChartDB',
|
visit_website: 'Truy cập ChartDB',
|
||||||
join_discord: 'Tham gia Discord',
|
join_discord: 'Tham gia Discord',
|
||||||
schedule_a_call: 'Trò chuyện cùng chúng tôi!',
|
schedule_a_call: 'Trò chuyện cùng chúng tôi!',
|
||||||
@@ -124,6 +125,12 @@ export const vi: LanguageTranslation = {
|
|||||||
add_table: 'Thêm bảng',
|
add_table: 'Thêm bảng',
|
||||||
filter: 'Lọc',
|
filter: 'Lọc',
|
||||||
collapse: 'Thu gọn tất cả',
|
collapse: 'Thu gọn tất cả',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Trường',
|
fields: 'Trường',
|
||||||
@@ -144,6 +151,8 @@ export const vi: LanguageTranslation = {
|
|||||||
comments: 'Bình luận',
|
comments: 'Bình luận',
|
||||||
no_comments: 'Không có bình luận',
|
no_comments: 'Không có bình luận',
|
||||||
delete_field: 'Xóa trường',
|
delete_field: 'Xóa trường',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Thuộc tính chỉ mục',
|
title: 'Thuộc tính chỉ mục',
|
||||||
@@ -370,6 +379,20 @@ export const vi: LanguageTranslation = {
|
|||||||
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? chartdb.io@gmail.com',
|
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Quan hệ một-một',
|
one_to_one: 'Quan hệ một-một',
|
||||||
one_to_many: 'Quan hệ một-nhiều',
|
one_to_many: 'Quan hệ một-nhiều',
|
||||||
@@ -386,6 +409,7 @@ export const vi: LanguageTranslation = {
|
|||||||
edit_table: 'Sửa bảng',
|
edit_table: 'Sửa bảng',
|
||||||
duplicate_table: 'Nhân đôi bảng',
|
duplicate_table: 'Nhân đôi bảng',
|
||||||
delete_table: 'Xóa bảng',
|
delete_table: 'Xóa bảng',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: 'Căn lưới (Giữ phím {{key}})',
|
snap_to_grid_tooltip: 'Căn lưới (Giữ phím {{key}})',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
new: '新建',
|
new: '新建',
|
||||||
open: '打开',
|
open: '打开',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
import_database: '导入数据库',
|
import: '导入数据库',
|
||||||
export_sql: '导出 SQL 语句',
|
export_sql: '导出 SQL 语句',
|
||||||
export_as: '导出为',
|
export_as: '导出为',
|
||||||
delete_diagram: '删除关系图',
|
delete_diagram: '删除关系图',
|
||||||
@@ -34,13 +34,14 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
backup: {
|
||||||
share: '分享',
|
backup: '备份',
|
||||||
export_diagram: '导出关系图',
|
export_diagram: '导出关系图',
|
||||||
import_diagram: '导入关系图',
|
restore_diagram: '还原图表',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: '帮助',
|
help: '帮助',
|
||||||
|
docs_website: '文档',
|
||||||
visit_website: '访问 ChartDB',
|
visit_website: '访问 ChartDB',
|
||||||
join_discord: '在 Discord 上加入我们',
|
join_discord: '在 Discord 上加入我们',
|
||||||
schedule_a_call: '和我们交流!',
|
schedule_a_call: '和我们交流!',
|
||||||
@@ -121,6 +122,12 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
add_table: '添加表',
|
add_table: '添加表',
|
||||||
filter: '筛选',
|
filter: '筛选',
|
||||||
collapse: '全部折叠',
|
collapse: '全部折叠',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: '字段',
|
fields: '字段',
|
||||||
@@ -141,6 +148,8 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
comments: '注释',
|
comments: '注释',
|
||||||
no_comments: '空',
|
no_comments: '空',
|
||||||
delete_field: '删除字段',
|
delete_field: '删除字段',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: '索引属性',
|
title: '索引属性',
|
||||||
@@ -366,6 +375,20 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
|
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: '一对一',
|
one_to_one: '一对一',
|
||||||
one_to_many: '一对多',
|
one_to_many: '一对多',
|
||||||
@@ -382,6 +405,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
edit_table: '编辑表',
|
edit_table: '编辑表',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
delete_table: '删除表',
|
delete_table: '删除表',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: '对齐到网格(按住 {{key}})',
|
snap_to_grid_tooltip: '对齐到网格(按住 {{key}})',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
new: '新增',
|
new: '新增',
|
||||||
open: '開啟',
|
open: '開啟',
|
||||||
save: '儲存',
|
save: '儲存',
|
||||||
import_database: '匯入資料庫',
|
import: '匯入資料庫',
|
||||||
export_sql: '匯出 SQL',
|
export_sql: '匯出 SQL',
|
||||||
export_as: '匯出為特定格式',
|
export_as: '匯出為特定格式',
|
||||||
delete_diagram: '刪除圖表',
|
delete_diagram: '刪除圖表',
|
||||||
@@ -34,13 +34,14 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
show_minimap: 'Show Mini Map',
|
show_minimap: 'Show Mini Map',
|
||||||
hide_minimap: 'Hide Mini Map',
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
backup: {
|
||||||
share: '分享',
|
backup: '備份',
|
||||||
export_diagram: '匯出圖表',
|
export_diagram: '匯出圖表',
|
||||||
import_diagram: '匯入圖表',
|
restore_diagram: '恢復圖表',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: '幫助',
|
help: '幫助',
|
||||||
|
docs_website: '文件',
|
||||||
visit_website: '訪問 ChartDB 網站',
|
visit_website: '訪問 ChartDB 網站',
|
||||||
join_discord: '加入 Discord',
|
join_discord: '加入 Discord',
|
||||||
schedule_a_call: '與我們聯絡!',
|
schedule_a_call: '與我們聯絡!',
|
||||||
@@ -121,6 +122,12 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
add_table: '新增表格',
|
add_table: '新增表格',
|
||||||
filter: '篩選',
|
filter: '篩選',
|
||||||
collapse: '全部摺疊',
|
collapse: '全部摺疊',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: '欄位',
|
fields: '欄位',
|
||||||
@@ -141,6 +148,8 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
comments: '註解',
|
comments: '註解',
|
||||||
no_comments: '無註解',
|
no_comments: '無註解',
|
||||||
delete_field: '刪除欄位',
|
delete_field: '刪除欄位',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: '索引屬性',
|
title: '索引屬性',
|
||||||
@@ -365,6 +374,20 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
|
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
example_title: 'Import Example DBML',
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
skip_and_empty: 'Skip & Empty',
|
||||||
|
show_example: 'Show Example',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to parse DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: '一對一',
|
one_to_one: '一對一',
|
||||||
one_to_many: '一對多',
|
one_to_many: '一對多',
|
||||||
@@ -381,6 +404,7 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
edit_table: '編輯表格',
|
edit_table: '編輯表格',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||||
delete_table: '刪除表格',
|
delete_table: '刪除表格',
|
||||||
|
add_relationship: 'Add Relationship', // TODO: Translate
|
||||||
},
|
},
|
||||||
|
|
||||||
snap_to_grid_tooltip: '對齊網格(按住 {{key}})',
|
snap_to_grid_tooltip: '對齊網格(按住 {{key}})',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const clickhouseDataTypes: readonly DataType[] = [
|
export const clickhouseDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'uint8', id: 'uint8' },
|
{ name: 'uint8', id: 'uint8' },
|
||||||
{ name: 'uint16', id: 'uint16' },
|
{ name: 'uint16', id: 'uint16' },
|
||||||
@@ -48,25 +48,41 @@ export const clickhouseDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'mediumblob', id: 'mediumblob' },
|
{ name: 'mediumblob', id: 'mediumblob' },
|
||||||
{ name: 'tinyblob', id: 'tinyblob' },
|
{ name: 'tinyblob', id: 'tinyblob' },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'char large object', id: 'char_large_object' },
|
{ name: 'char large object', id: 'char_large_object' },
|
||||||
{ name: 'char varying', id: 'char_varying' },
|
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
|
||||||
{ name: 'character large object', id: 'character_large_object' },
|
{ name: 'character large object', id: 'character_large_object' },
|
||||||
{ name: 'character varying', id: 'character_varying' },
|
{
|
||||||
|
name: 'character varying',
|
||||||
|
id: 'character_varying',
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
{ name: 'nchar large object', id: 'nchar_large_object' },
|
{ name: 'nchar large object', id: 'nchar_large_object' },
|
||||||
{ name: 'nchar varying', id: 'nchar_varying' },
|
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
|
||||||
{
|
{
|
||||||
name: 'national character large object',
|
name: 'national character large object',
|
||||||
id: 'national_character_large_object',
|
id: 'national_character_large_object',
|
||||||
},
|
},
|
||||||
{ name: 'national character varying', id: 'national_character_varying' },
|
{
|
||||||
{ name: 'national char varying', id: 'national_char_varying' },
|
name: 'national character varying',
|
||||||
{ name: 'national character', id: 'national_character' },
|
id: 'national_character_varying',
|
||||||
{ name: 'national char', id: 'national_char' },
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'national char varying',
|
||||||
|
id: 'national_char_varying',
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'national character',
|
||||||
|
id: 'national_character',
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
|
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
|
||||||
{ name: 'binary large object', id: 'binary_large_object' },
|
{ name: 'binary large object', id: 'binary_large_object' },
|
||||||
{ name: 'binary varying', id: 'binary_varying' },
|
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
|
||||||
{ name: 'fixedstring', id: 'fixedstring' },
|
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
|
||||||
{ name: 'string', id: 'string' },
|
{ name: 'string', id: 'string' },
|
||||||
|
|
||||||
// Date Types
|
// Date Types
|
||||||
|
|||||||
@@ -13,12 +13,16 @@ export interface DataType {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DataTypeData extends DataType {
|
||||||
|
hasCharMaxLength?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
|
export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
|
||||||
[DatabaseType.GENERIC]: genericDataTypes,
|
[DatabaseType.GENERIC]: genericDataTypes,
|
||||||
[DatabaseType.POSTGRESQL]: postgresDataTypes,
|
[DatabaseType.POSTGRESQL]: postgresDataTypes,
|
||||||
[DatabaseType.MYSQL]: mysqlDataTypes,
|
[DatabaseType.MYSQL]: mysqlDataTypes,
|
||||||
@@ -62,3 +66,23 @@ export function areFieldTypesCompatible(
|
|||||||
dbCompatibleTypes[type2.id]?.includes(type1.id)
|
dbCompatibleTypes[type2.id]?.includes(type1.id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const dataTypes = Object.values(dataTypeMap).flat();
|
||||||
|
|
||||||
|
export const dataTypeDataToDataType = (
|
||||||
|
dataTypeData: DataTypeData
|
||||||
|
): DataType => ({
|
||||||
|
id: dataTypeData.id,
|
||||||
|
name: dataTypeData.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const findDataTypeDataById = (
|
||||||
|
id: string,
|
||||||
|
databaseType?: DatabaseType
|
||||||
|
): DataTypeData | undefined => {
|
||||||
|
const dataTypesOptions = databaseType
|
||||||
|
? dataTypeMap[databaseType]
|
||||||
|
: dataTypes;
|
||||||
|
|
||||||
|
return dataTypesOptions.find((dataType) => dataType.id === id);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const genericDataTypes: readonly DataType[] = [
|
export const genericDataTypes: readonly DataTypeData[] = [
|
||||||
{ name: 'bigint', id: 'bigint' },
|
{ name: 'bigint', id: 'bigint' },
|
||||||
{ name: 'binary', id: 'binary' },
|
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'boolean', id: 'boolean' },
|
{ name: 'boolean', id: 'boolean' },
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'date', id: 'date' },
|
{ name: 'date', id: 'date' },
|
||||||
{ name: 'datetime', id: 'datetime' },
|
{ name: 'datetime', id: 'datetime' },
|
||||||
{ name: 'decimal', id: 'decimal' },
|
{ name: 'decimal', id: 'decimal' },
|
||||||
@@ -22,6 +22,6 @@ export const genericDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'time', id: 'time' },
|
{ name: 'time', id: 'time' },
|
||||||
{ name: 'timestamp', id: 'timestamp' },
|
{ name: 'timestamp', id: 'timestamp' },
|
||||||
{ name: 'uuid', id: 'uuid' },
|
{ name: 'uuid', id: 'uuid' },
|
||||||
{ name: 'varbinary', id: 'varbinary' },
|
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const mariadbDataTypes: readonly DataType[] = [
|
export const mariadbDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'tinyint', id: 'tinyint' },
|
{ name: 'tinyint', id: 'tinyint' },
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
@@ -23,10 +23,10 @@ export const mariadbDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'year', id: 'year' },
|
{ name: 'year', id: 'year' },
|
||||||
|
|
||||||
// String Types
|
// String Types
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'binary', id: 'binary' },
|
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||||
{ name: 'varbinary', id: 'varbinary' },
|
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||||
{ name: 'tinyblob', id: 'tinyblob' },
|
{ name: 'tinyblob', id: 'tinyblob' },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'mediumblob', id: 'mediumblob' },
|
{ name: 'mediumblob', id: 'mediumblob' },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const mysqlDataTypes: readonly DataType[] = [
|
export const mysqlDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'tinyint', id: 'tinyint' },
|
{ name: 'tinyint', id: 'tinyint' },
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
@@ -23,10 +23,10 @@ export const mysqlDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'year', id: 'year' },
|
{ name: 'year', id: 'year' },
|
||||||
|
|
||||||
// String Types
|
// String Types
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'binary', id: 'binary' },
|
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||||
{ name: 'varbinary', id: 'varbinary' },
|
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||||
{ name: 'tinyblob', id: 'tinyblob' },
|
{ name: 'tinyblob', id: 'tinyblob' },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'mediumblob', id: 'mediumblob' },
|
{ name: 'mediumblob', id: 'mediumblob' },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const postgresDataTypes: readonly DataType[] = [
|
export const postgresDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
{ name: 'integer', id: 'integer' },
|
{ name: 'integer', id: 'integer' },
|
||||||
@@ -15,9 +15,13 @@ export const postgresDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'money', id: 'money' },
|
{ name: 'money', id: 'money' },
|
||||||
|
|
||||||
// Character Types
|
// Character Types
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'character varying', id: 'character_varying' },
|
{
|
||||||
|
name: 'character varying',
|
||||||
|
id: 'character_varying',
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
{ name: 'text', id: 'text' },
|
{ name: 'text', id: 'text' },
|
||||||
|
|
||||||
// Binary Data Types
|
// Binary Data Types
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const sqlServerDataTypes: readonly DataType[] = [
|
export const sqlServerDataTypes: readonly DataTypeData[] = [
|
||||||
// Exact Numerics
|
// Exact Numerics
|
||||||
{ name: 'bigint', id: 'bigint' },
|
{ name: 'bigint', id: 'bigint' },
|
||||||
{ name: 'bit', id: 'bit' },
|
{ name: 'bit', id: 'bit' },
|
||||||
@@ -25,18 +25,18 @@ export const sqlServerDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'time', id: 'time' },
|
{ name: 'time', id: 'time' },
|
||||||
|
|
||||||
// Character Strings
|
// Character Strings
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'text', id: 'text' },
|
{ name: 'text', id: 'text' },
|
||||||
|
|
||||||
// Unicode Character Strings
|
// Unicode Character Strings
|
||||||
{ name: 'nchar', id: 'nchar' },
|
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
|
||||||
{ name: 'nvarchar', id: 'nvarchar' },
|
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true },
|
||||||
{ name: 'ntext', id: 'ntext' },
|
{ name: 'ntext', id: 'ntext' },
|
||||||
|
|
||||||
// Binary Strings
|
// Binary Strings
|
||||||
{ name: 'binary', id: 'binary' },
|
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||||
{ name: 'varbinary', id: 'varbinary' },
|
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||||
{ name: 'image', id: 'image' },
|
{ name: 'image', id: 'image' },
|
||||||
|
|
||||||
// Other Data Types
|
// Other Data Types
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const sqliteDataTypes: readonly DataType[] = [
|
export const sqliteDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'integer', id: 'integer' },
|
{ name: 'integer', id: 'integer' },
|
||||||
{ name: 'real', id: 'real' },
|
{ name: 'real', id: 'real' },
|
||||||
@@ -12,6 +12,9 @@ export const sqliteDataTypes: readonly DataType[] = [
|
|||||||
// Blob Type
|
// Blob Type
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
|
|
||||||
|
// Blob Type
|
||||||
|
{ name: 'json', id: 'json' },
|
||||||
|
|
||||||
// Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times)
|
// Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times)
|
||||||
{ name: 'date', id: 'date' },
|
{ name: 'date', id: 'date' },
|
||||||
{ name: 'datetime', id: 'datetime' },
|
{ name: 'datetime', id: 'datetime' },
|
||||||
@@ -19,6 +22,6 @@ export const sqliteDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'int', id: 'int' },
|
{ name: 'int', id: 'int' },
|
||||||
{ name: 'float', id: 'float' },
|
{ name: 'float', id: 'float' },
|
||||||
{ name: 'boolean', id: 'boolean' },
|
{ name: 'boolean', id: 'boolean' },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'decimal', id: 'decimal' },
|
{ name: 'decimal', id: 'decimal' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
82
src/lib/data/export-metadata/export-per-type/common.ts
Normal file
82
src/lib/data/export-metadata/export-per-type/common.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
|
||||||
|
export function isFunction(value: string): boolean {
|
||||||
|
// Common SQL functions
|
||||||
|
const functionPatterns = [
|
||||||
|
/^CURRENT_TIMESTAMP$/i,
|
||||||
|
/^NOW\(\)$/i,
|
||||||
|
/^GETDATE\(\)$/i,
|
||||||
|
/^CURRENT_DATE$/i,
|
||||||
|
/^CURRENT_TIME$/i,
|
||||||
|
/^UUID\(\)$/i,
|
||||||
|
/^NEWID\(\)$/i,
|
||||||
|
/^NEXT VALUE FOR/i,
|
||||||
|
/^IDENTITY\s*\(\d+,\s*\d+\)$/i,
|
||||||
|
];
|
||||||
|
return functionPatterns.some((pattern) => pattern.test(value.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isKeyword(value: string): boolean {
|
||||||
|
// Common SQL keywords that can be used as default values
|
||||||
|
const keywords = [
|
||||||
|
'NULL',
|
||||||
|
'TRUE',
|
||||||
|
'FALSE',
|
||||||
|
'CURRENT_TIMESTAMP',
|
||||||
|
'CURRENT_DATE',
|
||||||
|
'CURRENT_TIME',
|
||||||
|
'CURRENT_USER',
|
||||||
|
'SESSION_USER',
|
||||||
|
'SYSTEM_USER',
|
||||||
|
];
|
||||||
|
return keywords.includes(value.trim().toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function strHasQuotes(value: string): boolean {
|
||||||
|
return /^['"].*['"]$/.test(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportFieldComment(comment: string): string {
|
||||||
|
if (!comment) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment
|
||||||
|
.split('\n')
|
||||||
|
.map((commentLine) => ` -- ${commentLine}\n`)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInlineFK(table: DBTable, diagram: Diagram): string {
|
||||||
|
if (!diagram.relationships) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fks = diagram.relationships
|
||||||
|
.filter((r) => r.sourceTableId === table.id)
|
||||||
|
.map((r) => {
|
||||||
|
const targetTable = diagram.tables?.find(
|
||||||
|
(t) => t.id === r.targetTableId
|
||||||
|
);
|
||||||
|
const sourceField = table.fields.find(
|
||||||
|
(f) => f.id === r.sourceFieldId
|
||||||
|
);
|
||||||
|
const targetField = targetTable?.fields.find(
|
||||||
|
(f) => f.id === r.targetFieldId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!targetTable || !sourceField || !targetField) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetTableName = targetTable.schema
|
||||||
|
? `"${targetTable.schema}"."${targetTable.name}"`
|
||||||
|
: `"${targetTable.name}"`;
|
||||||
|
|
||||||
|
return ` FOREIGN KEY ("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}")`;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return fks.join(',\n');
|
||||||
|
}
|
||||||
247
src/lib/data/export-metadata/export-per-type/mssql.ts
Normal file
247
src/lib/data/export-metadata/export-per-type/mssql.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import {
|
||||||
|
exportFieldComment,
|
||||||
|
isFunction,
|
||||||
|
isKeyword,
|
||||||
|
strHasQuotes,
|
||||||
|
} from './common';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
function parseMSSQLDefault(field: DBField): string {
|
||||||
|
if (!field.default) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultValue = field.default.trim();
|
||||||
|
|
||||||
|
// Remove type casting for SQL Server
|
||||||
|
defaultValue = defaultValue.split('::')[0];
|
||||||
|
|
||||||
|
// Handle nextval sequences for SQL Server
|
||||||
|
if (defaultValue.includes('nextval')) {
|
||||||
|
return 'IDENTITY(1,1)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for SQL Server DEFAULT values
|
||||||
|
if (defaultValue.match(/^\(\(.*\)\)$/)) {
|
||||||
|
// Handle ((0)), ((0.00)) style defaults
|
||||||
|
return defaultValue.replace(/^\(\(|\)\)$/g, '');
|
||||||
|
} else if (defaultValue.match(/^\(N'.*'\)$/)) {
|
||||||
|
// Handle (N'value') style defaults
|
||||||
|
const innerValue = defaultValue.replace(/^\(N'|'\)$/g, '');
|
||||||
|
return `N'${innerValue}'`;
|
||||||
|
} else if (defaultValue.match(/^\(NULL\)$/i)) {
|
||||||
|
// Handle (NULL) defaults
|
||||||
|
return 'NULL';
|
||||||
|
} else if (defaultValue.match(/^\(getdate\(\)\)$/i)) {
|
||||||
|
// Handle (getdate()) defaults
|
||||||
|
return 'getdate()';
|
||||||
|
} else if (defaultValue.match(/^\('?\*'?\)$/i) || defaultValue === '*') {
|
||||||
|
// Handle ('*') or (*) or * defaults - common for "all" values
|
||||||
|
return "N'*'";
|
||||||
|
} else if (defaultValue.match(/^\((['"])(.*)\1\)$/)) {
|
||||||
|
// Handle ('value') or ("value") style defaults
|
||||||
|
const matches = defaultValue.match(/^\((['"])(.*)\1\)$/);
|
||||||
|
return matches ? `N'${matches[2]}'` : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle special characters that could be interpreted as operators
|
||||||
|
const sqlServerSpecialChars = /[*+\-/%&|^!=<>~]/;
|
||||||
|
if (sqlServerSpecialChars.test(defaultValue)) {
|
||||||
|
// If the value contains special characters and isn't already properly quoted
|
||||||
|
if (
|
||||||
|
!strHasQuotes(defaultValue) &&
|
||||||
|
!isFunction(defaultValue) &&
|
||||||
|
!isKeyword(defaultValue)
|
||||||
|
) {
|
||||||
|
return `N'${defaultValue.replace(/'/g, "''")}'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
strHasQuotes(defaultValue) ||
|
||||||
|
isFunction(defaultValue) ||
|
||||||
|
isKeyword(defaultValue) ||
|
||||||
|
/^-?\d+(\.\d+)?$/.test(defaultValue)
|
||||||
|
) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `'${defaultValue}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportMSSQL(diagram: Diagram): string {
|
||||||
|
if (!diagram.tables || !diagram.relationships) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tables = diagram.tables;
|
||||||
|
const relationships = diagram.relationships;
|
||||||
|
|
||||||
|
// Create CREATE SCHEMA statements for all schemas
|
||||||
|
let sqlScript = '';
|
||||||
|
const schemas = new Set<string>();
|
||||||
|
|
||||||
|
tables.forEach((table) => {
|
||||||
|
if (table.schema) {
|
||||||
|
schemas.add(table.schema);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add schema creation statements
|
||||||
|
schemas.forEach((schema) => {
|
||||||
|
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate table creation SQL
|
||||||
|
sqlScript += tables
|
||||||
|
.map((table: DBTable) => {
|
||||||
|
// Skip views
|
||||||
|
if (table.isView) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableName = table.schema
|
||||||
|
? `[${table.schema}].[${table.name}]`
|
||||||
|
: `[${table.name}]`;
|
||||||
|
|
||||||
|
return `${
|
||||||
|
table.comments ? `/**\n${table.comments}\n*/\n` : ''
|
||||||
|
}CREATE TABLE ${tableName} (\n${table.fields
|
||||||
|
.map((field: DBField) => {
|
||||||
|
const fieldName = `[${field.name}]`;
|
||||||
|
const typeName = field.type.name;
|
||||||
|
|
||||||
|
// Handle SQL Server specific type formatting
|
||||||
|
let typeWithSize = typeName;
|
||||||
|
if (field.characterMaximumLength) {
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'varchar' ||
|
||||||
|
typeName.toLowerCase() === 'nvarchar' ||
|
||||||
|
typeName.toLowerCase() === 'char' ||
|
||||||
|
typeName.toLowerCase() === 'nchar'
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||||
|
}
|
||||||
|
} else if (field.precision && field.scale) {
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'decimal' ||
|
||||||
|
typeName.toLowerCase() === 'numeric'
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
|
||||||
|
}
|
||||||
|
} else if (field.precision) {
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'decimal' ||
|
||||||
|
typeName.toLowerCase() === 'numeric'
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(${field.precision})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||||
|
|
||||||
|
// Check if identity column
|
||||||
|
const identity = field.default
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes('identity')
|
||||||
|
? ' IDENTITY(1,1)'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const unique =
|
||||||
|
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||||
|
|
||||||
|
// Handle default value using SQL Server specific parser
|
||||||
|
const defaultValue =
|
||||||
|
field.default &&
|
||||||
|
!field.default.toLowerCase().includes('identity')
|
||||||
|
? ` DEFAULT ${parseMSSQLDefault(field)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
|
||||||
|
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
|
||||||
|
})
|
||||||
|
.join(',\n')}${
|
||||||
|
table.fields.filter((f) => f.primaryKey).length > 0
|
||||||
|
? `,\n PRIMARY KEY (${table.fields
|
||||||
|
.filter((f) => f.primaryKey)
|
||||||
|
.map((f) => `[${f.name}]`)
|
||||||
|
.join(', ')})`
|
||||||
|
: ''
|
||||||
|
}\n);\n\n${table.indexes
|
||||||
|
.map((index) => {
|
||||||
|
const indexName = table.schema
|
||||||
|
? `[${table.schema}_${index.name}]`
|
||||||
|
: `[${index.name}]`;
|
||||||
|
const indexFields = index.fieldIds
|
||||||
|
.map((fieldId) => {
|
||||||
|
const field = table.fields.find(
|
||||||
|
(f) => f.id === fieldId
|
||||||
|
);
|
||||||
|
return field ? `[${field.name}]` : '';
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// SQL Server has a limit of 32 columns in an index
|
||||||
|
if (indexFields.length > 32) {
|
||||||
|
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
|
||||||
|
console.warn(
|
||||||
|
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
|
||||||
|
);
|
||||||
|
indexFields.length = 32;
|
||||||
|
return indexFields.length > 0
|
||||||
|
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexFields.length > 0
|
||||||
|
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
|
||||||
|
: '';
|
||||||
|
})
|
||||||
|
.join('')}`;
|
||||||
|
})
|
||||||
|
.filter(Boolean) // Remove empty strings (views)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// Generate foreign keys
|
||||||
|
sqlScript += `\n${relationships
|
||||||
|
.map((r: DBRelationship) => {
|
||||||
|
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||||
|
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!sourceTable ||
|
||||||
|
!targetTable ||
|
||||||
|
sourceTable.isView ||
|
||||||
|
targetTable.isView
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceField = sourceTable.fields.find(
|
||||||
|
(f) => f.id === r.sourceFieldId
|
||||||
|
);
|
||||||
|
const targetField = targetTable.fields.find(
|
||||||
|
(f) => f.id === r.targetFieldId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceField || !targetField) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceTableName = sourceTable.schema
|
||||||
|
? `[${sourceTable.schema}].[${sourceTable.name}]`
|
||||||
|
: `[${sourceTable.name}]`;
|
||||||
|
const targetTableName = targetTable.schema
|
||||||
|
? `[${targetTable.schema}].[${targetTable.name}]`
|
||||||
|
: `[${targetTable.name}]`;
|
||||||
|
|
||||||
|
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`;
|
||||||
|
})
|
||||||
|
.filter(Boolean) // Remove empty strings
|
||||||
|
.join('\n')}`;
|
||||||
|
|
||||||
|
return sqlScript;
|
||||||
|
}
|
||||||
447
src/lib/data/export-metadata/export-per-type/mysql.ts
Normal file
447
src/lib/data/export-metadata/export-per-type/mysql.ts
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
import {
|
||||||
|
exportFieldComment,
|
||||||
|
isFunction,
|
||||||
|
isKeyword,
|
||||||
|
strHasQuotes,
|
||||||
|
} from './common';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
function parseMySQLDefault(field: DBField): string {
|
||||||
|
if (!field.default) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue = field.default.trim();
|
||||||
|
|
||||||
|
// Handle specific MySQL default values
|
||||||
|
if (
|
||||||
|
defaultValue.toLowerCase() === 'now()' ||
|
||||||
|
defaultValue.toLowerCase() === 'current_timestamp'
|
||||||
|
) {
|
||||||
|
return 'CURRENT_TIMESTAMP';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle MySQL auto-increment, which is handled via AUTO_INCREMENT
|
||||||
|
if (
|
||||||
|
defaultValue.toLowerCase().includes('identity') ||
|
||||||
|
defaultValue.toLowerCase().includes('autoincrement') ||
|
||||||
|
defaultValue.includes('nextval')
|
||||||
|
) {
|
||||||
|
return ''; // MySQL handles this with AUTO_INCREMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a function call, convert to MySQL equivalents
|
||||||
|
if (isFunction(defaultValue)) {
|
||||||
|
// Map common PostgreSQL/MSSQL functions to MySQL equivalents
|
||||||
|
if (
|
||||||
|
defaultValue.toLowerCase().includes('newid()') ||
|
||||||
|
defaultValue.toLowerCase().includes('uuid()')
|
||||||
|
) {
|
||||||
|
return 'UUID()';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For functions we can't translate, return as is (MySQL might not support them)
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a keyword, keep it as is
|
||||||
|
if (isKeyword(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it already has quotes, keep it as is
|
||||||
|
if (strHasQuotes(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a number, keep it as is
|
||||||
|
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other cases, add quotes
|
||||||
|
return `'${defaultValue.replace(/'/g, "''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map problematic types to MySQL compatible types
|
||||||
|
function mapMySQLType(typeName: string): string {
|
||||||
|
typeName = typeName.toLowerCase();
|
||||||
|
|
||||||
|
// Map common types to MySQL type system
|
||||||
|
switch (typeName) {
|
||||||
|
case 'int':
|
||||||
|
case 'integer':
|
||||||
|
return 'INT';
|
||||||
|
|
||||||
|
case 'smallint':
|
||||||
|
return 'SMALLINT';
|
||||||
|
|
||||||
|
case 'bigint':
|
||||||
|
return 'BIGINT';
|
||||||
|
|
||||||
|
case 'decimal':
|
||||||
|
case 'numeric':
|
||||||
|
return 'DECIMAL';
|
||||||
|
|
||||||
|
case 'float':
|
||||||
|
return 'FLOAT';
|
||||||
|
|
||||||
|
case 'double':
|
||||||
|
case 'real':
|
||||||
|
return 'DOUBLE';
|
||||||
|
|
||||||
|
case 'char':
|
||||||
|
case 'character':
|
||||||
|
return 'CHAR';
|
||||||
|
|
||||||
|
case 'varchar':
|
||||||
|
case 'character varying':
|
||||||
|
case 'nvarchar':
|
||||||
|
return 'VARCHAR';
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
case 'ntext':
|
||||||
|
return 'TEXT';
|
||||||
|
|
||||||
|
case 'longtext':
|
||||||
|
return 'LONGTEXT';
|
||||||
|
|
||||||
|
case 'mediumtext':
|
||||||
|
return 'MEDIUMTEXT';
|
||||||
|
|
||||||
|
case 'tinytext':
|
||||||
|
return 'TINYTEXT';
|
||||||
|
|
||||||
|
case 'date':
|
||||||
|
return 'DATE';
|
||||||
|
|
||||||
|
case 'datetime':
|
||||||
|
case 'timestamp':
|
||||||
|
case 'datetime2':
|
||||||
|
return 'DATETIME';
|
||||||
|
|
||||||
|
case 'time':
|
||||||
|
return 'TIME';
|
||||||
|
|
||||||
|
case 'blob':
|
||||||
|
case 'binary':
|
||||||
|
return 'BLOB';
|
||||||
|
|
||||||
|
case 'varbinary':
|
||||||
|
return 'VARBINARY';
|
||||||
|
|
||||||
|
case 'bit':
|
||||||
|
return 'BIT';
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
case 'bool':
|
||||||
|
return 'TINYINT(1)'; // MySQL uses TINYINT(1) for boolean
|
||||||
|
|
||||||
|
case 'enum':
|
||||||
|
return 'VARCHAR(50)'; // Convert ENUM to VARCHAR instead of assuming values
|
||||||
|
|
||||||
|
case 'json':
|
||||||
|
case 'jsonb':
|
||||||
|
return 'JSON'; // MySQL has JSON type since 5.7.8
|
||||||
|
|
||||||
|
case 'uuid':
|
||||||
|
return 'CHAR(36)'; // MySQL doesn't have a UUID type, use CHAR(36)
|
||||||
|
|
||||||
|
case 'geometry':
|
||||||
|
case 'geography':
|
||||||
|
return 'GEOMETRY'; // If MySQL has spatial extensions
|
||||||
|
|
||||||
|
case 'array':
|
||||||
|
case 'user-defined':
|
||||||
|
return 'JSON'; // Use JSON for complex types like arrays or user-defined
|
||||||
|
}
|
||||||
|
|
||||||
|
// If type has array notation (ends with []), treat as JSON
|
||||||
|
if (typeName.endsWith('[]')) {
|
||||||
|
return 'JSON';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other types, default to original type
|
||||||
|
return typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportMySQL(diagram: Diagram): string {
|
||||||
|
if (!diagram.tables || !diagram.relationships) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tables = diagram.tables;
|
||||||
|
const relationships = diagram.relationships;
|
||||||
|
|
||||||
|
// Start SQL script
|
||||||
|
let sqlScript = '-- MySQL database export\n\n';
|
||||||
|
|
||||||
|
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
|
||||||
|
sqlScript += 'START TRANSACTION;\n\n';
|
||||||
|
|
||||||
|
// Create databases (schemas) if they don't exist
|
||||||
|
const schemas = new Set<string>();
|
||||||
|
tables.forEach((table) => {
|
||||||
|
if (table.schema) {
|
||||||
|
schemas.add(table.schema);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
schemas.forEach((schema) => {
|
||||||
|
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (schemas.size > 0) {
|
||||||
|
sqlScript += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate table creation SQL
|
||||||
|
sqlScript += tables
|
||||||
|
.map((table: DBTable) => {
|
||||||
|
// Skip views
|
||||||
|
if (table.isView) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use schema prefix if available
|
||||||
|
const tableName = table.schema
|
||||||
|
? `\`${table.schema}\`.\`${table.name}\``
|
||||||
|
: `\`${table.name}\``;
|
||||||
|
|
||||||
|
// Get primary key fields
|
||||||
|
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
||||||
|
|
||||||
|
return `${
|
||||||
|
table.comments ? `-- ${table.comments}\n` : ''
|
||||||
|
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||||
|
.map((field: DBField) => {
|
||||||
|
const fieldName = `\`${field.name}\``;
|
||||||
|
|
||||||
|
// Handle type name - map to MySQL compatible types
|
||||||
|
const typeName = mapMySQLType(field.type.name);
|
||||||
|
|
||||||
|
// Handle MySQL specific type formatting
|
||||||
|
let typeWithSize = typeName;
|
||||||
|
if (field.characterMaximumLength) {
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'varchar' ||
|
||||||
|
typeName.toLowerCase() === 'char' ||
|
||||||
|
typeName.toLowerCase() === 'varbinary'
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||||
|
}
|
||||||
|
} else if (field.precision && field.scale) {
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'decimal' ||
|
||||||
|
typeName.toLowerCase() === 'numeric'
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
|
||||||
|
}
|
||||||
|
} else if (field.precision) {
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'decimal' ||
|
||||||
|
typeName.toLowerCase() === 'numeric'
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(${field.precision})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a default size for VARCHAR columns if not specified
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'varchar' &&
|
||||||
|
!field.characterMaximumLength
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(255)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||||
|
|
||||||
|
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
|
||||||
|
let autoIncrement = '';
|
||||||
|
if (
|
||||||
|
field.primaryKey &&
|
||||||
|
(field.default?.toLowerCase().includes('identity') ||
|
||||||
|
field.default
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes('autoincrement') ||
|
||||||
|
field.default?.includes('nextval'))
|
||||||
|
) {
|
||||||
|
autoIncrement = ' AUTO_INCREMENT';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add UNIQUE constraint if the field is not part of the primary key
|
||||||
|
const unique =
|
||||||
|
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||||
|
|
||||||
|
// Handle default value
|
||||||
|
const defaultValue =
|
||||||
|
field.default &&
|
||||||
|
!field.default.toLowerCase().includes('identity') &&
|
||||||
|
!field.default
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('autoincrement') &&
|
||||||
|
!field.default.includes('nextval')
|
||||||
|
? ` DEFAULT ${parseMySQLDefault(field)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// MySQL supports inline comments
|
||||||
|
const comment = field.comments
|
||||||
|
? ` COMMENT '${field.comments.replace(/'/g, "''")}'`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
|
||||||
|
})
|
||||||
|
.join(',\n')}${
|
||||||
|
// Add PRIMARY KEY as table constraint
|
||||||
|
primaryKeyFields.length > 0
|
||||||
|
? `,\n PRIMARY KEY (${primaryKeyFields
|
||||||
|
.map((f) => `\`${f.name}\``)
|
||||||
|
.join(', ')})`
|
||||||
|
: ''
|
||||||
|
}\n)${
|
||||||
|
// MySQL supports table comments
|
||||||
|
table.comments
|
||||||
|
? ` COMMENT='${table.comments.replace(/'/g, "''")}'`
|
||||||
|
: ''
|
||||||
|
};\n\n${
|
||||||
|
// Add indexes - MySQL creates them separately from the table definition
|
||||||
|
table.indexes
|
||||||
|
.map((index) => {
|
||||||
|
// Get the list of fields for this index
|
||||||
|
const indexFields = index.fieldIds
|
||||||
|
.map((fieldId) => {
|
||||||
|
const field = table.fields.find(
|
||||||
|
(f) => f.id === fieldId
|
||||||
|
);
|
||||||
|
return field ? field : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Skip if this index exactly matches the primary key fields
|
||||||
|
if (
|
||||||
|
primaryKeyFields.length === indexFields.length &&
|
||||||
|
primaryKeyFields.every((pk) =>
|
||||||
|
indexFields.some(
|
||||||
|
(field) => field && field.id === pk.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique index name by combining table name, field names, and a unique/non-unique indicator
|
||||||
|
const fieldNamesForIndex = indexFields
|
||||||
|
.map((field) => field?.name || '')
|
||||||
|
.join('_');
|
||||||
|
const uniqueIndicator = index.unique ? '_unique' : '';
|
||||||
|
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
|
||||||
|
|
||||||
|
// Get the properly quoted field names
|
||||||
|
const indexFieldNames = indexFields
|
||||||
|
.map((field) => (field ? `\`${field.name}\`` : ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Check for text/blob fields that need special handling
|
||||||
|
const hasTextOrBlob = indexFields.some((field) => {
|
||||||
|
const typeName =
|
||||||
|
field?.type.name.toLowerCase() || '';
|
||||||
|
return (
|
||||||
|
typeName === 'text' ||
|
||||||
|
typeName === 'mediumtext' ||
|
||||||
|
typeName === 'longtext' ||
|
||||||
|
typeName === 'blob'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there are TEXT/BLOB fields, need to add prefix length
|
||||||
|
const indexFieldsWithPrefix = hasTextOrBlob
|
||||||
|
? indexFieldNames.map((name) => {
|
||||||
|
const field = indexFields.find(
|
||||||
|
(f) => `\`${f?.name}\`` === name
|
||||||
|
);
|
||||||
|
if (!field) return name;
|
||||||
|
|
||||||
|
const typeName =
|
||||||
|
field.type.name.toLowerCase();
|
||||||
|
if (
|
||||||
|
typeName === 'text' ||
|
||||||
|
typeName === 'mediumtext' ||
|
||||||
|
typeName === 'longtext' ||
|
||||||
|
typeName === 'blob'
|
||||||
|
) {
|
||||||
|
// Add a prefix length for TEXT/BLOB fields (required in MySQL)
|
||||||
|
return `${name}(255)`;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
})
|
||||||
|
: indexFieldNames;
|
||||||
|
|
||||||
|
return indexFieldNames.length > 0
|
||||||
|
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldsWithPrefix.join(', ')});\n`
|
||||||
|
: '';
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
|
}`;
|
||||||
|
})
|
||||||
|
.filter(Boolean) // Remove empty strings (views)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// Generate foreign keys
|
||||||
|
if (relationships.length > 0) {
|
||||||
|
sqlScript += '\n-- Foreign key constraints\n\n';
|
||||||
|
|
||||||
|
sqlScript += relationships
|
||||||
|
.map((r: DBRelationship) => {
|
||||||
|
const sourceTable = tables.find(
|
||||||
|
(t) => t.id === r.sourceTableId
|
||||||
|
);
|
||||||
|
const targetTable = tables.find(
|
||||||
|
(t) => t.id === r.targetTableId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!sourceTable ||
|
||||||
|
!targetTable ||
|
||||||
|
sourceTable.isView ||
|
||||||
|
targetTable.isView
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceField = sourceTable.fields.find(
|
||||||
|
(f) => f.id === r.sourceFieldId
|
||||||
|
);
|
||||||
|
const targetField = targetTable.fields.find(
|
||||||
|
(f) => f.id === r.targetFieldId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceField || !targetField) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceTableName = sourceTable.schema
|
||||||
|
? `\`${sourceTable.schema}\`.\`${sourceTable.name}\``
|
||||||
|
: `\`${sourceTable.name}\``;
|
||||||
|
const targetTableName = targetTable.schema
|
||||||
|
? `\`${targetTable.schema}\`.\`${targetTable.name}\``
|
||||||
|
: `\`${targetTable.name}\``;
|
||||||
|
|
||||||
|
// Create a descriptive constraint name
|
||||||
|
const constraintName = `\`fk_${sourceTable.name}_${sourceField.name}\``;
|
||||||
|
|
||||||
|
// MySQL supports ON DELETE and ON UPDATE actions
|
||||||
|
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${sourceField.name}\`) REFERENCES ${targetTableName}(\`${targetField.name}\`)\nON UPDATE CASCADE ON DELETE RESTRICT;\n`;
|
||||||
|
})
|
||||||
|
.filter(Boolean) // Remove empty strings
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
sqlScript += '\nCOMMIT;\n';
|
||||||
|
|
||||||
|
return sqlScript;
|
||||||
|
}
|
||||||
364
src/lib/data/export-metadata/export-per-type/postgresql.ts
Normal file
364
src/lib/data/export-metadata/export-per-type/postgresql.ts
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
import {
|
||||||
|
exportFieldComment,
|
||||||
|
isFunction,
|
||||||
|
isKeyword,
|
||||||
|
strHasQuotes,
|
||||||
|
} from './common';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
function parsePostgresDefault(field: DBField): string {
|
||||||
|
if (!field.default) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue = field.default.trim();
|
||||||
|
|
||||||
|
// Handle specific PostgreSQL default values
|
||||||
|
if (defaultValue.toLowerCase() === 'now()') {
|
||||||
|
return 'CURRENT_TIMESTAMP';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle PostgreSQL functions for JSON/JSONB types
|
||||||
|
if (
|
||||||
|
(field.type.name.toLowerCase() === 'json' ||
|
||||||
|
field.type.name.toLowerCase() === 'jsonb') &&
|
||||||
|
(defaultValue.includes('json_build_object') ||
|
||||||
|
defaultValue.includes('jsonb_build_object') ||
|
||||||
|
defaultValue.includes('json_build_array') ||
|
||||||
|
defaultValue.includes('jsonb_build_array') ||
|
||||||
|
defaultValue.includes('to_json') ||
|
||||||
|
defaultValue.includes('to_jsonb'))
|
||||||
|
) {
|
||||||
|
// Remove any enclosing quotes and return the function call as is
|
||||||
|
return defaultValue.replace(/^'(.*)'$/, '$1').replace(/''/, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nextval sequences for PostgreSQL
|
||||||
|
if (defaultValue.includes('nextval')) {
|
||||||
|
return defaultValue; // Keep it as is for PostgreSQL
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a function call, keep it as is
|
||||||
|
if (isFunction(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a keyword, keep it as is
|
||||||
|
if (isKeyword(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it already has quotes, keep it as is
|
||||||
|
if (strHasQuotes(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a number, keep it as is
|
||||||
|
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other cases, add quotes
|
||||||
|
return `'${defaultValue.replace(/'/g, "''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map problematic types to PostgreSQL compatible types
|
||||||
|
function mapPostgresType(typeName: string, fieldName: string): string {
|
||||||
|
typeName = typeName.toLowerCase();
|
||||||
|
fieldName = fieldName.toLowerCase();
|
||||||
|
|
||||||
|
// Handle known problematic types
|
||||||
|
if (typeName === 'user-defined') {
|
||||||
|
return 'jsonb'; // Default fallback for user-defined types
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle generic "array" type (when not specified as array of what)
|
||||||
|
if (typeName === 'array') {
|
||||||
|
return 'text[]'; // Default to text array
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle array type notation
|
||||||
|
if (typeName.endsWith('[]')) {
|
||||||
|
const baseType = mapPostgresType(typeName.slice(0, -2), fieldName);
|
||||||
|
return `${baseType}[]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default case: return the type as is
|
||||||
|
return typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportPostgreSQL(diagram: Diagram): string {
|
||||||
|
if (!diagram.tables || !diagram.relationships) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tables = diagram.tables;
|
||||||
|
const relationships = diagram.relationships;
|
||||||
|
|
||||||
|
// Create CREATE SCHEMA statements for all schemas
|
||||||
|
let sqlScript = '';
|
||||||
|
const schemas = new Set<string>();
|
||||||
|
|
||||||
|
tables.forEach((table) => {
|
||||||
|
if (table.schema) {
|
||||||
|
schemas.add(table.schema);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add schema creation statements
|
||||||
|
schemas.forEach((schema) => {
|
||||||
|
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
|
||||||
|
});
|
||||||
|
sqlScript += '\n';
|
||||||
|
|
||||||
|
// Add sequence creation statements
|
||||||
|
const sequences = new Set<string>();
|
||||||
|
|
||||||
|
tables.forEach((table) => {
|
||||||
|
table.fields.forEach((field) => {
|
||||||
|
if (field.default) {
|
||||||
|
// Match nextval('schema.sequence_name') or nextval('sequence_name')
|
||||||
|
const match = field.default.match(
|
||||||
|
/nextval\('([^']+)'(?:::[^)]+)?\)/
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
sequences.add(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sequences.forEach((sequence) => {
|
||||||
|
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
|
||||||
|
});
|
||||||
|
sqlScript += '\n';
|
||||||
|
|
||||||
|
// Generate table creation SQL
|
||||||
|
sqlScript += tables
|
||||||
|
.map((table: DBTable) => {
|
||||||
|
// Skip views
|
||||||
|
if (table.isView) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableName = table.schema
|
||||||
|
? `"${table.schema}"."${table.name}"`
|
||||||
|
: `"${table.name}"`;
|
||||||
|
|
||||||
|
// Get primary key fields
|
||||||
|
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
||||||
|
|
||||||
|
return `${
|
||||||
|
table.comments ? `-- ${table.comments}\n` : ''
|
||||||
|
}CREATE TABLE ${tableName} (\n${table.fields
|
||||||
|
.map((field: DBField) => {
|
||||||
|
const fieldName = `"${field.name}"`;
|
||||||
|
|
||||||
|
// Handle type name - map problematic types to PostgreSQL compatible types
|
||||||
|
const typeName = mapPostgresType(
|
||||||
|
field.type.name,
|
||||||
|
field.name
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle PostgreSQL specific type formatting
|
||||||
|
let typeWithSize = typeName;
|
||||||
|
if (field.characterMaximumLength) {
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'varchar' ||
|
||||||
|
typeName.toLowerCase() === 'character varying' ||
|
||||||
|
typeName.toLowerCase() === 'char' ||
|
||||||
|
typeName.toLowerCase() === 'character'
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||||
|
}
|
||||||
|
} else if (field.precision && field.scale) {
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'decimal' ||
|
||||||
|
typeName.toLowerCase() === 'numeric'
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
|
||||||
|
}
|
||||||
|
} else if (field.precision) {
|
||||||
|
if (
|
||||||
|
typeName.toLowerCase() === 'decimal' ||
|
||||||
|
typeName.toLowerCase() === 'numeric'
|
||||||
|
) {
|
||||||
|
typeWithSize = `${typeName}(${field.precision})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle array types (check if the type name ends with '[]')
|
||||||
|
if (typeName.endsWith('[]')) {
|
||||||
|
typeWithSize = typeWithSize.replace('[]', '') + '[]';
|
||||||
|
}
|
||||||
|
|
||||||
|
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||||
|
|
||||||
|
// Handle identity generation
|
||||||
|
let identity = '';
|
||||||
|
if (field.default && field.default.includes('nextval')) {
|
||||||
|
// PostgreSQL already handles this with DEFAULT nextval()
|
||||||
|
} else if (
|
||||||
|
field.default &&
|
||||||
|
field.default.toLowerCase().includes('identity')
|
||||||
|
) {
|
||||||
|
identity = ' GENERATED BY DEFAULT AS IDENTITY';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add UNIQUE constraint if the field is not part of the primary key
|
||||||
|
// This avoids redundant uniqueness constraints
|
||||||
|
const unique =
|
||||||
|
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||||
|
|
||||||
|
// Handle default value using PostgreSQL specific parser
|
||||||
|
const defaultValue =
|
||||||
|
field.default &&
|
||||||
|
!field.default.toLowerCase().includes('identity')
|
||||||
|
? ` DEFAULT ${parsePostgresDefault(field)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
|
||||||
|
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
|
||||||
|
})
|
||||||
|
.join(',\n')}${
|
||||||
|
primaryKeyFields.length > 0
|
||||||
|
? `,\n PRIMARY KEY (${primaryKeyFields
|
||||||
|
.map((f) => `"${f.name}"`)
|
||||||
|
.join(', ')})`
|
||||||
|
: ''
|
||||||
|
}\n);\n\n${
|
||||||
|
// Add table comments
|
||||||
|
table.comments
|
||||||
|
? `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n\n`
|
||||||
|
: ''
|
||||||
|
}${
|
||||||
|
// Add column comments
|
||||||
|
table.fields
|
||||||
|
.filter((f) => f.comments)
|
||||||
|
.map(
|
||||||
|
(f) =>
|
||||||
|
`COMMENT ON COLUMN ${tableName}."${f.name}" IS '${f.comments?.replace(/'/g, "''")}';\n`
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
}\n${
|
||||||
|
// Add indexes only for non-primary key fields or composite indexes
|
||||||
|
// This avoids duplicate indexes on primary key columns
|
||||||
|
table.indexes
|
||||||
|
.map((index) => {
|
||||||
|
// Get the list of fields for this index
|
||||||
|
const indexFields = index.fieldIds
|
||||||
|
.map((fieldId) => {
|
||||||
|
const field = table.fields.find(
|
||||||
|
(f) => f.id === fieldId
|
||||||
|
);
|
||||||
|
return field ? field : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Skip if this index exactly matches the primary key fields
|
||||||
|
// This prevents creating redundant indexes
|
||||||
|
if (
|
||||||
|
primaryKeyFields.length === indexFields.length &&
|
||||||
|
primaryKeyFields.every((pk) =>
|
||||||
|
indexFields.some(
|
||||||
|
(field) => field && field.id === pk.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create unique index name using table name and index name
|
||||||
|
// This ensures index names are unique across the database
|
||||||
|
const safeTableName = table.name.replace(
|
||||||
|
/[^a-zA-Z0-9_]/g,
|
||||||
|
'_'
|
||||||
|
);
|
||||||
|
const safeIndexName = index.name.replace(
|
||||||
|
/[^a-zA-Z0-9_]/g,
|
||||||
|
'_'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Limit index name length to avoid PostgreSQL's 63-character identifier limit
|
||||||
|
let combinedName = `${safeTableName}_${safeIndexName}`;
|
||||||
|
if (combinedName.length > 60) {
|
||||||
|
// If too long, use just the index name or a truncated version
|
||||||
|
combinedName =
|
||||||
|
safeIndexName.length > 60
|
||||||
|
? safeIndexName.substring(0, 60)
|
||||||
|
: safeIndexName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexName = `"${combinedName}"`;
|
||||||
|
|
||||||
|
// Get the properly quoted field names
|
||||||
|
const indexFieldNames = indexFields
|
||||||
|
.map((field) => (field ? `"${field.name}"` : ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return indexFieldNames.length > 0
|
||||||
|
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldNames.join(', ')});\n\n`
|
||||||
|
: '';
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('')
|
||||||
|
}`;
|
||||||
|
})
|
||||||
|
.filter(Boolean) // Remove empty strings (views)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// Generate foreign keys
|
||||||
|
sqlScript += `\n${relationships
|
||||||
|
.map((r: DBRelationship) => {
|
||||||
|
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||||
|
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!sourceTable ||
|
||||||
|
!targetTable ||
|
||||||
|
sourceTable.isView ||
|
||||||
|
targetTable.isView
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceField = sourceTable.fields.find(
|
||||||
|
(f) => f.id === r.sourceFieldId
|
||||||
|
);
|
||||||
|
const targetField = targetTable.fields.find(
|
||||||
|
(f) => f.id === r.targetFieldId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceField || !targetField) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceTableName = sourceTable.schema
|
||||||
|
? `"${sourceTable.schema}"."${sourceTable.name}"`
|
||||||
|
: `"${sourceTable.name}"`;
|
||||||
|
const targetTableName = targetTable.schema
|
||||||
|
? `"${targetTable.schema}"."${targetTable.name}"`
|
||||||
|
: `"${targetTable.name}"`;
|
||||||
|
|
||||||
|
// Create a unique constraint name by combining table and field names
|
||||||
|
// Ensure it stays within PostgreSQL's 63-character limit for identifiers
|
||||||
|
// and doesn't get truncated in a way that breaks SQL syntax
|
||||||
|
const baseName = `fk_${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`;
|
||||||
|
// Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
|
||||||
|
const safeConstraintName =
|
||||||
|
baseName.length > 60
|
||||||
|
? baseName.substring(0, 60).replace(/[^a-zA-Z0-9_]/g, '_')
|
||||||
|
: baseName.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||||
|
|
||||||
|
const constraintName = `"${safeConstraintName}"`;
|
||||||
|
|
||||||
|
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}");\n`;
|
||||||
|
})
|
||||||
|
.filter(Boolean) // Remove empty strings
|
||||||
|
.join('\n')}`;
|
||||||
|
|
||||||
|
return sqlScript;
|
||||||
|
}
|
||||||
358
src/lib/data/export-metadata/export-per-type/sqlite.ts
Normal file
358
src/lib/data/export-metadata/export-per-type/sqlite.ts
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
import {
|
||||||
|
exportFieldComment,
|
||||||
|
isFunction,
|
||||||
|
isKeyword,
|
||||||
|
strHasQuotes,
|
||||||
|
} from './common';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
function parseSQLiteDefault(field: DBField): string {
|
||||||
|
if (!field.default) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue = field.default.trim();
|
||||||
|
|
||||||
|
// Handle specific SQLite default values
|
||||||
|
if (
|
||||||
|
defaultValue.toLowerCase() === 'now()' ||
|
||||||
|
defaultValue.toLowerCase() === 'current_timestamp'
|
||||||
|
) {
|
||||||
|
return 'CURRENT_TIMESTAMP';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SQLite auto-increment
|
||||||
|
if (
|
||||||
|
defaultValue.toLowerCase().includes('identity') ||
|
||||||
|
defaultValue.toLowerCase().includes('autoincrement') ||
|
||||||
|
defaultValue.includes('nextval')
|
||||||
|
) {
|
||||||
|
return ''; // SQLite handles this differently with INTEGER PRIMARY KEY AUTOINCREMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a function call, convert to SQLite equivalents
|
||||||
|
if (isFunction(defaultValue)) {
|
||||||
|
// Map common PostgreSQL/MSSQL functions to SQLite equivalents
|
||||||
|
if (
|
||||||
|
defaultValue.toLowerCase().includes('newid()') ||
|
||||||
|
defaultValue.toLowerCase().includes('uuid()')
|
||||||
|
) {
|
||||||
|
return 'lower(hex(randomblob(16)))';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For functions we can't translate, return as is (SQLite might not support them)
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a keyword, keep it as is
|
||||||
|
if (isKeyword(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it already has quotes, keep it as is
|
||||||
|
if (strHasQuotes(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a number, keep it as is
|
||||||
|
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other cases, add quotes
|
||||||
|
return `'${defaultValue.replace(/'/g, "''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map problematic types to SQLite compatible types
|
||||||
|
function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
|
||||||
|
typeName = typeName.toLowerCase();
|
||||||
|
|
||||||
|
// Special handling for primary key integer columns (autoincrement requires INTEGER PRIMARY KEY)
|
||||||
|
if (isPrimaryKey && (typeName === 'integer' || typeName === 'int')) {
|
||||||
|
return 'INTEGER'; // Must be uppercase for SQLite to recognize it for AUTOINCREMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map common types to SQLite's simplified type system
|
||||||
|
switch (typeName) {
|
||||||
|
case 'int':
|
||||||
|
case 'smallint':
|
||||||
|
case 'tinyint':
|
||||||
|
case 'mediumint':
|
||||||
|
case 'bigint':
|
||||||
|
return 'INTEGER';
|
||||||
|
|
||||||
|
case 'decimal':
|
||||||
|
case 'numeric':
|
||||||
|
case 'float':
|
||||||
|
case 'double':
|
||||||
|
case 'real':
|
||||||
|
return 'REAL';
|
||||||
|
|
||||||
|
case 'char':
|
||||||
|
case 'nchar':
|
||||||
|
case 'varchar':
|
||||||
|
case 'nvarchar':
|
||||||
|
case 'text':
|
||||||
|
case 'ntext':
|
||||||
|
case 'character varying':
|
||||||
|
case 'character':
|
||||||
|
return 'TEXT';
|
||||||
|
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
case 'timestamp':
|
||||||
|
case 'datetime2':
|
||||||
|
return 'TEXT'; // SQLite doesn't have dedicated date types
|
||||||
|
|
||||||
|
case 'blob':
|
||||||
|
case 'binary':
|
||||||
|
case 'varbinary':
|
||||||
|
case 'image':
|
||||||
|
return 'BLOB';
|
||||||
|
|
||||||
|
case 'bit':
|
||||||
|
case 'boolean':
|
||||||
|
return 'INTEGER'; // SQLite doesn't have a boolean type, use INTEGER
|
||||||
|
|
||||||
|
case 'user-defined':
|
||||||
|
case 'json':
|
||||||
|
case 'jsonb':
|
||||||
|
return 'TEXT'; // Store as JSON text
|
||||||
|
|
||||||
|
case 'array':
|
||||||
|
return 'TEXT'; // Store as serialized array text
|
||||||
|
|
||||||
|
case 'geometry':
|
||||||
|
case 'geography':
|
||||||
|
return 'BLOB'; // Store spatial data as BLOB in SQLite
|
||||||
|
}
|
||||||
|
|
||||||
|
// If type has array notation (ends with []), treat as TEXT
|
||||||
|
if (typeName.endsWith('[]')) {
|
||||||
|
return 'TEXT';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other types, default to TEXT
|
||||||
|
return typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportSQLite(diagram: Diagram): string {
|
||||||
|
if (!diagram.tables || !diagram.relationships) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tables = diagram.tables;
|
||||||
|
const relationships = diagram.relationships;
|
||||||
|
|
||||||
|
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
|
||||||
|
let sqlScript = '-- SQLite database export\n\n';
|
||||||
|
|
||||||
|
// Begin transaction for faster import
|
||||||
|
sqlScript += 'BEGIN TRANSACTION;\n\n';
|
||||||
|
|
||||||
|
// SQLite doesn't have sequences, so we skip sequence creation
|
||||||
|
|
||||||
|
// SQLite system tables that should be skipped
|
||||||
|
const sqliteSystemTables = [
|
||||||
|
'sqlite_sequence',
|
||||||
|
'sqlite_stat1',
|
||||||
|
'sqlite_stat2',
|
||||||
|
'sqlite_stat3',
|
||||||
|
'sqlite_stat4',
|
||||||
|
'sqlite_master',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate table creation SQL
|
||||||
|
sqlScript += tables
|
||||||
|
.map((table: DBTable) => {
|
||||||
|
// Skip views
|
||||||
|
if (table.isView) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip SQLite system tables
|
||||||
|
if (sqliteSystemTables.includes(table.name.toLowerCase())) {
|
||||||
|
return `-- Skipping SQLite system table: "${table.name}"\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLite doesn't use schema prefixes, so we use just the table name
|
||||||
|
// Include the schema in a comment if it exists
|
||||||
|
const schemaComment = table.schema
|
||||||
|
? `-- Original schema: ${table.schema}\n`
|
||||||
|
: '';
|
||||||
|
const tableName = `"${table.name}"`;
|
||||||
|
|
||||||
|
// Get primary key fields
|
||||||
|
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
||||||
|
|
||||||
|
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
|
||||||
|
const singleIntegerPrimaryKey =
|
||||||
|
primaryKeyFields.length === 1 &&
|
||||||
|
(primaryKeyFields[0].type.name.toLowerCase() === 'integer' ||
|
||||||
|
primaryKeyFields[0].type.name.toLowerCase() === 'int');
|
||||||
|
|
||||||
|
return `${schemaComment}${
|
||||||
|
table.comments ? `-- ${table.comments}\n` : ''
|
||||||
|
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||||
|
.map((field: DBField) => {
|
||||||
|
const fieldName = `"${field.name}"`;
|
||||||
|
|
||||||
|
// Handle type name - map to SQLite compatible types
|
||||||
|
const typeName = mapSQLiteType(
|
||||||
|
field.type.name,
|
||||||
|
field.primaryKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// SQLite ignores length specifiers, so we don't add them
|
||||||
|
// We'll keep this simple without size info
|
||||||
|
const typeWithoutSize = typeName;
|
||||||
|
|
||||||
|
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||||
|
|
||||||
|
// Handle autoincrement - only works with INTEGER PRIMARY KEY
|
||||||
|
let autoIncrement = '';
|
||||||
|
if (
|
||||||
|
field.primaryKey &&
|
||||||
|
singleIntegerPrimaryKey &&
|
||||||
|
(field.default?.toLowerCase().includes('identity') ||
|
||||||
|
field.default
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes('autoincrement') ||
|
||||||
|
field.default?.includes('nextval'))
|
||||||
|
) {
|
||||||
|
autoIncrement = ' AUTOINCREMENT';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add UNIQUE constraint if the field is not part of the primary key
|
||||||
|
const unique =
|
||||||
|
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||||
|
|
||||||
|
// Handle default value - Special handling for datetime() function
|
||||||
|
let defaultValue = '';
|
||||||
|
if (
|
||||||
|
field.default &&
|
||||||
|
!field.default.toLowerCase().includes('identity') &&
|
||||||
|
!field.default
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('autoincrement') &&
|
||||||
|
!field.default.includes('nextval')
|
||||||
|
) {
|
||||||
|
// Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes
|
||||||
|
if (field.default.includes("datetime(''now'')")) {
|
||||||
|
defaultValue = ' DEFAULT CURRENT_TIMESTAMP';
|
||||||
|
} else {
|
||||||
|
defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add PRIMARY KEY inline only for single INTEGER primary key
|
||||||
|
const primaryKey =
|
||||||
|
field.primaryKey && singleIntegerPrimaryKey
|
||||||
|
? ' PRIMARY KEY' + autoIncrement
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
|
||||||
|
})
|
||||||
|
.join(',\n')}${
|
||||||
|
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
|
||||||
|
primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
|
||||||
|
? `,\n PRIMARY KEY (${primaryKeyFields
|
||||||
|
.map((f) => `"${f.name}"`)
|
||||||
|
.join(', ')})`
|
||||||
|
: ''
|
||||||
|
}\n);\n\n${
|
||||||
|
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
|
||||||
|
table.indexes
|
||||||
|
.map((index) => {
|
||||||
|
// Skip indexes that exactly match the primary key
|
||||||
|
const indexFields = index.fieldIds
|
||||||
|
.map((fieldId) => {
|
||||||
|
const field = table.fields.find(
|
||||||
|
(f) => f.id === fieldId
|
||||||
|
);
|
||||||
|
return field ? field : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Get the properly quoted field names
|
||||||
|
const indexFieldNames = indexFields
|
||||||
|
.map((field) => (field ? `"${field.name}"` : ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Skip if this index exactly matches the primary key fields
|
||||||
|
if (
|
||||||
|
primaryKeyFields.length === indexFields.length &&
|
||||||
|
primaryKeyFields.every((pk) =>
|
||||||
|
indexFields.some(
|
||||||
|
(field) => field && field.id === pk.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create safe index name
|
||||||
|
const safeIndexName = `${table.name}_${index.name}`
|
||||||
|
.replace(/[^a-zA-Z0-9_]/g, '_')
|
||||||
|
.substring(0, 60);
|
||||||
|
|
||||||
|
return indexFieldNames.length > 0
|
||||||
|
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});\n`
|
||||||
|
: '';
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
|
}`;
|
||||||
|
})
|
||||||
|
.filter(Boolean) // Remove empty strings (views)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// Generate table constraints and triggers for foreign keys
|
||||||
|
// SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
|
||||||
|
// But we'll also provide individual ALTER TABLE statements as comments for reference
|
||||||
|
|
||||||
|
if (relationships.length > 0) {
|
||||||
|
sqlScript += '\n-- Foreign key constraints\n';
|
||||||
|
sqlScript +=
|
||||||
|
'-- Note: SQLite requires foreign_keys pragma to be enabled:\n';
|
||||||
|
sqlScript += '-- PRAGMA foreign_keys = ON;\n\n';
|
||||||
|
|
||||||
|
relationships.forEach((r: DBRelationship) => {
|
||||||
|
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||||
|
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!sourceTable ||
|
||||||
|
!targetTable ||
|
||||||
|
sourceTable.isView ||
|
||||||
|
targetTable.isView ||
|
||||||
|
sqliteSystemTables.includes(sourceTable.name.toLowerCase()) ||
|
||||||
|
sqliteSystemTables.includes(targetTable.name.toLowerCase())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceField = sourceTable.fields.find(
|
||||||
|
(f) => f.id === r.sourceFieldId
|
||||||
|
);
|
||||||
|
const targetField = targetTable.fields.find(
|
||||||
|
(f) => f.id === r.targetFieldId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceField || !targetField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create commented out version of what would be ALTER TABLE statement
|
||||||
|
sqlScript += `-- ALTER TABLE "${sourceTable.name}" ADD CONSTRAINT "fk_${sourceTable.name}_${sourceField.name}" FOREIGN KEY("${sourceField.name}") REFERENCES "${targetTable.name}"("${targetField.name}");\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
sqlScript += '\nCOMMIT;\n';
|
||||||
|
|
||||||
|
return sqlScript;
|
||||||
|
}
|
||||||
@@ -1,17 +1,45 @@
|
|||||||
import type { Diagram } from '../../domain/diagram';
|
import type { Diagram } from '../../domain/diagram';
|
||||||
import { OPENAI_API_KEY } from '@/lib/env';
|
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
|
||||||
import type { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import type { DataType } from '../data-types/data-types';
|
import type { DataType } from '../data-types/data-types';
|
||||||
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
|
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
|
||||||
|
import { exportMSSQL } from './export-per-type/mssql';
|
||||||
|
import { exportPostgreSQL } from './export-per-type/postgresql';
|
||||||
|
import { exportSQLite } from './export-per-type/sqlite';
|
||||||
|
import { exportMySQL } from './export-per-type/mysql';
|
||||||
|
|
||||||
export const exportBaseSQL = (diagram: Diagram): string => {
|
export const exportBaseSQL = ({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType,
|
||||||
|
isDBMLFlow = false,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
targetDatabaseType: DatabaseType;
|
||||||
|
isDBMLFlow?: boolean;
|
||||||
|
}): string => {
|
||||||
const { tables, relationships } = diagram;
|
const { tables, relationships } = diagram;
|
||||||
|
|
||||||
if (!tables || tables.length === 0) {
|
if (!tables || tables.length === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
|
||||||
|
switch (diagram.databaseType) {
|
||||||
|
case DatabaseType.SQL_SERVER:
|
||||||
|
return exportMSSQL(diagram);
|
||||||
|
case DatabaseType.POSTGRESQL:
|
||||||
|
return exportPostgreSQL(diagram);
|
||||||
|
case DatabaseType.SQLITE:
|
||||||
|
return exportSQLite(diagram);
|
||||||
|
case DatabaseType.MYSQL:
|
||||||
|
case DatabaseType.MARIADB:
|
||||||
|
return exportMySQL(diagram);
|
||||||
|
default:
|
||||||
|
return exportPostgreSQL(diagram);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Filter out the tables that are views
|
// Filter out the tables that are views
|
||||||
const nonViewTables = tables.filter((table) => !table.isView);
|
const nonViewTables = tables.filter((table) => !table.isView);
|
||||||
|
|
||||||
@@ -67,6 +95,11 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
|||||||
table.fields.forEach((field, index) => {
|
table.fields.forEach((field, index) => {
|
||||||
let typeName = field.type.name;
|
let typeName = field.type.name;
|
||||||
|
|
||||||
|
// Handle ENUM type
|
||||||
|
if (typeName.toLowerCase() === 'enum') {
|
||||||
|
typeName = 'varchar';
|
||||||
|
}
|
||||||
|
|
||||||
// Temp fix for 'array' to be text[]
|
// Temp fix for 'array' to be text[]
|
||||||
if (typeName.toLowerCase() === 'array') {
|
if (typeName.toLowerCase() === 'array') {
|
||||||
typeName = 'text[]';
|
typeName = 'text[]';
|
||||||
@@ -110,8 +143,22 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
|||||||
|
|
||||||
// Remove the type cast part after :: if it exists
|
// Remove the type cast part after :: if it exists
|
||||||
if (fieldDefault.includes('::')) {
|
if (fieldDefault.includes('::')) {
|
||||||
|
const endedWithParentheses = fieldDefault.endsWith(')');
|
||||||
fieldDefault = fieldDefault.split('::')[0];
|
fieldDefault = fieldDefault.split('::')[0];
|
||||||
|
|
||||||
|
if (
|
||||||
|
(fieldDefault.startsWith('(') &&
|
||||||
|
!fieldDefault.endsWith(')')) ||
|
||||||
|
endedWithParentheses
|
||||||
|
) {
|
||||||
|
fieldDefault += ')';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fieldDefault === `('now')`) {
|
||||||
|
fieldDefault = `now()`;
|
||||||
|
}
|
||||||
|
|
||||||
sqlScript += ` DEFAULT ${fieldDefault}`;
|
sqlScript += ` DEFAULT ${fieldDefault}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +243,26 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
|||||||
return sqlScript;
|
return sqlScript;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateConfiguration = () => {
|
||||||
|
const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
|
||||||
|
const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT;
|
||||||
|
const modelName = window?.env?.LLM_MODEL_NAME ?? LLM_MODEL_NAME;
|
||||||
|
|
||||||
|
// If using custom endpoint and model, don't require OpenAI API key
|
||||||
|
if (baseUrl && modelName) {
|
||||||
|
return { useCustomEndpoint: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If using OpenAI's service, require API key
|
||||||
|
if (apiKey) {
|
||||||
|
return { useCustomEndpoint: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'Configuration Error: Either provide an OpenAI API key or both a custom endpoint and model name'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const exportSQL = async (
|
export const exportSQL = async (
|
||||||
diagram: Diagram,
|
diagram: Diagram,
|
||||||
databaseType: DatabaseType,
|
databaseType: DatabaseType,
|
||||||
@@ -205,7 +272,15 @@ export const exportSQL = async (
|
|||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const sqlScript = exportBaseSQL(diagram);
|
const sqlScript = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: databaseType,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (databaseType === diagram.databaseType) {
|
||||||
|
return sqlScript;
|
||||||
|
}
|
||||||
|
|
||||||
const cacheKey = await generateCacheKey(databaseType, sqlScript);
|
const cacheKey = await generateCacheKey(databaseType, sqlScript);
|
||||||
|
|
||||||
const cachedResult = getFromCache(cacheKey);
|
const cachedResult = getFromCache(cacheKey);
|
||||||
@@ -213,43 +288,76 @@ export const exportSQL = async (
|
|||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate configuration before proceeding
|
||||||
|
const { useCustomEndpoint } = validateConfiguration();
|
||||||
|
|
||||||
const [{ streamText, generateText }, { createOpenAI }] = await Promise.all([
|
const [{ streamText, generateText }, { createOpenAI }] = await Promise.all([
|
||||||
import('ai'),
|
import('ai'),
|
||||||
import('@ai-sdk/openai'),
|
import('@ai-sdk/openai'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const openai = createOpenAI({
|
const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
|
||||||
apiKey: window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY,
|
const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT;
|
||||||
});
|
const modelName =
|
||||||
|
window?.env?.LLM_MODEL_NAME ??
|
||||||
|
LLM_MODEL_NAME ??
|
||||||
|
'gpt-4o-mini-2024-07-18';
|
||||||
|
|
||||||
|
let config: { apiKey: string; baseUrl?: string };
|
||||||
|
|
||||||
|
if (useCustomEndpoint) {
|
||||||
|
config = {
|
||||||
|
apiKey: apiKey,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
config = {
|
||||||
|
apiKey: apiKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const openai = createOpenAI(config);
|
||||||
|
|
||||||
const prompt = generateSQLPrompt(databaseType, sqlScript);
|
const prompt = generateSQLPrompt(databaseType, sqlScript);
|
||||||
|
|
||||||
if (options?.stream) {
|
try {
|
||||||
const { textStream, text: textPromise } = await streamText({
|
if (options?.stream) {
|
||||||
model: openai('gpt-4o-mini-2024-07-18'),
|
const { textStream, text: textPromise } = await streamText({
|
||||||
|
model: openai(modelName),
|
||||||
|
prompt: prompt,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const textPart of textStream) {
|
||||||
|
if (options.signal?.aborted) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
options.onResultStream(textPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await textPromise;
|
||||||
|
|
||||||
|
setInCache(cacheKey, text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text } = await generateText({
|
||||||
|
model: openai(modelName),
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const textPart of textStream) {
|
|
||||||
if (options.signal?.aborted) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
options.onResultStream(textPart);
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = await textPromise;
|
|
||||||
|
|
||||||
setInCache(cacheKey, text);
|
setInCache(cacheKey, text);
|
||||||
return text;
|
return text;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Error generating SQL:', error);
|
||||||
|
if (error instanceof Error && error.message.includes('API key')) {
|
||||||
|
throw new Error(
|
||||||
|
'Error: Please check your API configuration. If using a custom endpoint, make sure the endpoint URL is correct.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'Error generating SQL script. Please check your configuration and try again.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text } = await generateText({
|
|
||||||
model: openai('gpt-4o-mini-2024-07-18'),
|
|
||||||
prompt: prompt,
|
|
||||||
});
|
|
||||||
|
|
||||||
setInCache(cacheKey, text);
|
|
||||||
return text;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMySQLDataTypeSize(type: DataType) {
|
function getMySQLDataTypeSize(type: DataType) {
|
||||||
@@ -333,7 +441,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
|
|||||||
- **Sequence Creation**: Use \`CREATE SEQUENCE IF NOT EXISTS\` for sequence creation.
|
- **Sequence Creation**: Use \`CREATE SEQUENCE IF NOT EXISTS\` for sequence creation.
|
||||||
- **Table and Index Creation**: Use \`CREATE TABLE IF NOT EXISTS\` and \`CREATE INDEX IF NOT EXISTS\` to avoid errors if the object already exists.
|
- **Table and Index Creation**: Use \`CREATE TABLE IF NOT EXISTS\` and \`CREATE INDEX IF NOT EXISTS\` to avoid errors if the object already exists.
|
||||||
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
||||||
- **Conditional Statements**: Utilize PostgreSQL’s support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
||||||
`,
|
`,
|
||||||
mysql: `
|
mysql: `
|
||||||
- **Table Creation**: Use \`CREATE TABLE IF NOT EXISTS\` for creating tables. While creating the table structure, ensure that all foreign key columns use the correct data types as determined in the foreign key review.
|
- **Table Creation**: Use \`CREATE TABLE IF NOT EXISTS\` for creating tables. While creating the table structure, ensure that all foreign key columns use the correct data types as determined in the foreign key review.
|
||||||
@@ -353,7 +461,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
|
|||||||
sql_server: `
|
sql_server: `
|
||||||
- **Sequence Creation**: Use \`CREATE SEQUENCE\` without \`IF NOT EXISTS\`, and employ conditional logic (\`IF NOT EXISTS\`) to check for sequence existence before creation.
|
- **Sequence Creation**: Use \`CREATE SEQUENCE\` without \`IF NOT EXISTS\`, and employ conditional logic (\`IF NOT EXISTS\`) to check for sequence existence before creation.
|
||||||
- **Identity Columns**: Always prefer using the \`IDENTITY\` keyword (e.g., \`INT IDENTITY(1,1)\`) for auto-incrementing primary key columns when possible.
|
- **Identity Columns**: Always prefer using the \`IDENTITY\` keyword (e.g., \`INT IDENTITY(1,1)\`) for auto-incrementing primary key columns when possible.
|
||||||
- **Conditional Logic**: Use a conditional block like \`IF NOT EXISTS (SELECT * FROM sys.objects WHERE ...)\` since SQL Server doesn’t support \`IF NOT EXISTS\` directly in \`CREATE\` statements.
|
- **Conditional Logic**: Use a conditional block like \`IF NOT EXISTS (SELECT * FROM sys.objects WHERE ...)\` since SQL Server doesn't support \`IF NOT EXISTS\` directly in \`CREATE\` statements.
|
||||||
- **Avoid Unsupported Syntax**: Ensure the script does not include unsupported statements like \`CREATE TABLE IF NOT EXISTS\`.
|
- **Avoid Unsupported Syntax**: Ensure the script does not include unsupported statements like \`CREATE TABLE IF NOT EXISTS\`.
|
||||||
|
|
||||||
**Reminder**: Ensure all column names that conflict with reserved keywords or data types (e.g., key, primary, column, table), escape the column name by enclosing it.
|
**Reminder**: Ensure all column names that conflict with reserved keywords or data types (e.g., key, primary, column, table), escape the column name by enclosing it.
|
||||||
@@ -387,7 +495,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
|
|||||||
- **Sequence Creation**: Use \`CREATE SEQUENCE IF NOT EXISTS\` for sequence creation.
|
- **Sequence Creation**: Use \`CREATE SEQUENCE IF NOT EXISTS\` for sequence creation.
|
||||||
- **Table and Index Creation**: Use \`CREATE TABLE IF NOT EXISTS\` and \`CREATE INDEX IF NOT EXISTS\` to avoid errors if the object already exists.
|
- **Table and Index Creation**: Use \`CREATE TABLE IF NOT EXISTS\` and \`CREATE INDEX IF NOT EXISTS\` to avoid errors if the object already exists.
|
||||||
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
||||||
- **Conditional Statements**: Utilize PostgreSQL’s support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, inde
|
|||||||
if (options.databaseClient === DatabaseClient.POSTGRESQL_PSQL) {
|
if (options.databaseClient === DatabaseClient.POSTGRESQL_PSQL) {
|
||||||
return `${psqlPreCommand}psql -h HOST_NAME -p PORT -U USER_NAME -d DATABASE_NAME -c "
|
return `${psqlPreCommand}psql -h HOST_NAME -p PORT -U USER_NAME -d DATABASE_NAME -c "
|
||||||
${query.replace(/"/g, '\\"').replace(/\\\\/g, '\\\\\\').replace(/\\x/g, '\\\\x')}
|
${query.replace(/"/g, '\\"').replace(/\\\\/g, '\\\\\\').replace(/\\x/g, '\\\\x')}
|
||||||
" -t -A | pbcopy; LG='\\033[0;32m'; NC='\\033[0m'; echo "You got the resultset ($(pbpaste | wc -c | xargs) characters) in Copy/Paste. \${LG}Go back & paste in ChartDB :)\${NC}";`;
|
" -t -A > output.json;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import { getPostgresQuery } from './postgres-script';
|
import { getPostgresQuery } from './postgres-script';
|
||||||
import { getMySQLQuery } from './mysql-script';
|
import { getMySQLQuery } from './mysql-script';
|
||||||
import { sqliteQuery } from './sqlite-script';
|
import { getSQLiteQuery } from './sqlite-script';
|
||||||
import { getSqlServerQuery } from './sqlserver-script';
|
import { getSqlServerQuery } from './sqlserver-script';
|
||||||
import { mariaDBQuery } from './maria-script';
|
import { mariaDBQuery } from './maria-script';
|
||||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
@@ -21,7 +21,7 @@ export const importMetadataScripts: ImportMetadataScripts = {
|
|||||||
[DatabaseType.GENERIC]: () => '',
|
[DatabaseType.GENERIC]: () => '',
|
||||||
[DatabaseType.POSTGRESQL]: getPostgresQuery,
|
[DatabaseType.POSTGRESQL]: getPostgresQuery,
|
||||||
[DatabaseType.MYSQL]: getMySQLQuery,
|
[DatabaseType.MYSQL]: getMySQLQuery,
|
||||||
[DatabaseType.SQLITE]: () => sqliteQuery,
|
[DatabaseType.SQLITE]: getSQLiteQuery,
|
||||||
[DatabaseType.SQL_SERVER]: getSqlServerQuery,
|
[DatabaseType.SQL_SERVER]: getSqlServerQuery,
|
||||||
[DatabaseType.MARIADB]: () => mariaDBQuery,
|
[DatabaseType.MARIADB]: () => mariaDBQuery,
|
||||||
[DatabaseType.CLICKHOUSE]: () => clickhouseQuery,
|
[DatabaseType.CLICKHOUSE]: () => clickhouseQuery,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
export const sqliteQuery = `WITH fk_info AS (
|
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
|
import { DatabaseClient } from '@/lib/domain/database-clients';
|
||||||
|
|
||||||
|
const sqliteQuery = `${`/* Standard SQLite */`}
|
||||||
|
WITH fk_info AS (
|
||||||
SELECT
|
SELECT
|
||||||
json_group_array(
|
json_group_array(
|
||||||
json_object(
|
json_object(
|
||||||
@@ -85,7 +89,7 @@ export const sqliteQuery = `WITH fk_info AS (
|
|||||||
ELSE LOWER(p.type)
|
ELSE LOWER(p.type)
|
||||||
END,
|
END,
|
||||||
'ordinal_position', p.cid,
|
'ordinal_position', p.cid,
|
||||||
'nullable', (CASE WHEN p."notnull" = 0 THEN 'true' ELSE 'false' END),
|
'nullable', (CASE WHEN p."notnull" = 0 THEN true ELSE false END),
|
||||||
'collation', '',
|
'collation', '',
|
||||||
'character_maximum_length',
|
'character_maximum_length',
|
||||||
CASE
|
CASE
|
||||||
@@ -163,3 +167,225 @@ replace(replace(replace(
|
|||||||
'\\"', '"'),'"[', '['), ']"', ']'
|
'\\"', '"'),'"[', '['), ']"', ']'
|
||||||
) AS metadata_json_to_import;
|
) AS metadata_json_to_import;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const cloudflareD1Query = `${`/* Cloudflare D1 SQLite */`}
|
||||||
|
WITH fk_info AS (
|
||||||
|
SELECT
|
||||||
|
json_group_array(
|
||||||
|
json_object(
|
||||||
|
'schema', '',
|
||||||
|
'table', m.name,
|
||||||
|
'column', fk.[from],
|
||||||
|
'foreign_key_name',
|
||||||
|
'fk_' || m.name || '_' || fk.[from] || '_' || fk.[table] || '_' || fk.[to],
|
||||||
|
'reference_schema', '',
|
||||||
|
'reference_table', fk.[table],
|
||||||
|
'reference_column', fk.[to],
|
||||||
|
'fk_def',
|
||||||
|
'FOREIGN KEY (' || fk.[from] || ') REFERENCES ' || fk.[table] || '(' || fk.[to] || ')' ||
|
||||||
|
' ON UPDATE ' || fk.on_update || ' ON DELETE ' || fk.on_delete
|
||||||
|
)
|
||||||
|
) AS fk_metadata
|
||||||
|
FROM
|
||||||
|
sqlite_master m
|
||||||
|
JOIN
|
||||||
|
pragma_foreign_key_list(m.name) fk
|
||||||
|
ON
|
||||||
|
m.type = 'table'
|
||||||
|
WHERE
|
||||||
|
m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
|
||||||
|
), pk_info AS (
|
||||||
|
SELECT
|
||||||
|
json_group_array(
|
||||||
|
json_object(
|
||||||
|
'schema', '',
|
||||||
|
'table', pk.table_name,
|
||||||
|
'field_count', pk.field_count,
|
||||||
|
'column', pk.pk_column,
|
||||||
|
'pk_def', 'PRIMARY KEY (' || pk.pk_column || ')'
|
||||||
|
)
|
||||||
|
) AS pk_metadata
|
||||||
|
FROM
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
m.name AS table_name,
|
||||||
|
COUNT(p.name) AS field_count,
|
||||||
|
GROUP_CONCAT(p.name) AS pk_column
|
||||||
|
FROM
|
||||||
|
sqlite_master m
|
||||||
|
JOIN
|
||||||
|
pragma_table_info(m.name) p
|
||||||
|
ON
|
||||||
|
m.type = 'table' AND p.pk > 0
|
||||||
|
WHERE
|
||||||
|
m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
|
||||||
|
GROUP BY
|
||||||
|
m.name
|
||||||
|
) pk
|
||||||
|
), indexes_metadata AS (
|
||||||
|
SELECT
|
||||||
|
json_group_array(
|
||||||
|
json_object(
|
||||||
|
'schema', '',
|
||||||
|
'table', m.name,
|
||||||
|
'name', idx.name,
|
||||||
|
'column', ic.name,
|
||||||
|
'index_type', 'B-TREE',
|
||||||
|
'cardinality', '',
|
||||||
|
'size', '',
|
||||||
|
'unique', (CASE WHEN idx.[unique] = 1 THEN 'true' ELSE 'false' END),
|
||||||
|
'direction', '',
|
||||||
|
'column_position', ic.seqno + 1
|
||||||
|
)
|
||||||
|
) AS indexes_metadata
|
||||||
|
FROM
|
||||||
|
sqlite_master m
|
||||||
|
JOIN
|
||||||
|
pragma_index_list(m.name) idx
|
||||||
|
ON
|
||||||
|
m.type = 'table'
|
||||||
|
JOIN
|
||||||
|
pragma_index_info(idx.name) ic
|
||||||
|
WHERE
|
||||||
|
m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
|
||||||
|
), cols AS (
|
||||||
|
SELECT
|
||||||
|
json_group_array(
|
||||||
|
json_object(
|
||||||
|
'schema', '',
|
||||||
|
'table', m.name,
|
||||||
|
'name', p.name,
|
||||||
|
'type',
|
||||||
|
CASE
|
||||||
|
WHEN INSTR(LOWER(p.type), '(') > 0 THEN
|
||||||
|
SUBSTR(LOWER(p.type), 1, INSTR(LOWER(p.type), '(') - 1)
|
||||||
|
ELSE LOWER(p.type)
|
||||||
|
END,
|
||||||
|
'ordinal_position', p.cid,
|
||||||
|
'nullable', (CASE WHEN p.[notnull] = 0 THEN true ELSE false END),
|
||||||
|
'collation', '',
|
||||||
|
'character_maximum_length',
|
||||||
|
CASE
|
||||||
|
WHEN LOWER(p.type) LIKE 'char%' OR LOWER(p.type) LIKE 'varchar%' THEN
|
||||||
|
CASE
|
||||||
|
WHEN INSTR(p.type, '(') > 0 THEN
|
||||||
|
REPLACE(SUBSTR(p.type, INSTR(p.type, '(') + 1, LENGTH(p.type) - INSTR(p.type, '(') - 1), ')', '')
|
||||||
|
ELSE 'null'
|
||||||
|
END
|
||||||
|
ELSE 'null'
|
||||||
|
END,
|
||||||
|
'precision',
|
||||||
|
CASE
|
||||||
|
WHEN LOWER(p.type) LIKE 'decimal%' OR LOWER(p.type) LIKE 'numeric%' THEN
|
||||||
|
CASE
|
||||||
|
WHEN instr(p.type, '(') > 0 THEN
|
||||||
|
json_object(
|
||||||
|
'precision', substr(p.type, instr(p.type, '(') + 1, instr(p.type, ',') - instr(p.type, '(') - 1),
|
||||||
|
'scale', substr(p.type, instr(p.type, ',') + 1, instr(p.type, ')') - instr(p.type, ',') - 1)
|
||||||
|
)
|
||||||
|
ELSE 'null'
|
||||||
|
END
|
||||||
|
ELSE 'null'
|
||||||
|
END,
|
||||||
|
'default', COALESCE(REPLACE(p.dflt_value, '"', '\\"'), '')
|
||||||
|
)
|
||||||
|
) AS cols_metadata
|
||||||
|
FROM
|
||||||
|
sqlite_master m
|
||||||
|
JOIN
|
||||||
|
pragma_table_info(m.name) p
|
||||||
|
ON
|
||||||
|
m.type in ('table', 'view')
|
||||||
|
WHERE
|
||||||
|
m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
|
||||||
|
), tbls AS (
|
||||||
|
SELECT
|
||||||
|
json_group_array(
|
||||||
|
json_object(
|
||||||
|
'schema', '',
|
||||||
|
'table', m.name,
|
||||||
|
'rows', -1,
|
||||||
|
'type', 'table',
|
||||||
|
'engine', '',
|
||||||
|
'collation', ''
|
||||||
|
)
|
||||||
|
) AS tbls_metadata
|
||||||
|
FROM
|
||||||
|
sqlite_master m
|
||||||
|
WHERE
|
||||||
|
m.type in ('table', 'view') AND m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
|
||||||
|
), views AS (
|
||||||
|
SELECT
|
||||||
|
json_group_array(
|
||||||
|
json_object(
|
||||||
|
'schema', '',
|
||||||
|
'view_name', m.name
|
||||||
|
)
|
||||||
|
) AS views_metadata
|
||||||
|
FROM
|
||||||
|
sqlite_master m
|
||||||
|
WHERE
|
||||||
|
m.type = 'view' AND m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
replace(replace(replace(
|
||||||
|
json_object(
|
||||||
|
'fk_info', (SELECT fk_metadata FROM fk_info),
|
||||||
|
'pk_info', (SELECT pk_metadata FROM pk_info),
|
||||||
|
'columns', (SELECT cols_metadata FROM cols),
|
||||||
|
'indexes', (SELECT indexes_metadata FROM indexes_metadata),
|
||||||
|
'tables', (SELECT tbls_metadata FROM tbls),
|
||||||
|
'views', (SELECT views_metadata FROM views),
|
||||||
|
'database_name', 'sqlite',
|
||||||
|
'version', ''
|
||||||
|
),
|
||||||
|
'\\"', '"'),'"[', '['), ']"', ']'
|
||||||
|
) AS metadata_json_to_import;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Generate Wrangler CLI command wrapper around the D1 query
|
||||||
|
const generateWranglerCommand = (): string => {
|
||||||
|
return `# Cloudflare D1 (via Wrangler CLI) Import Script
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# This query will extract your D1 database schema using Cloudflare's Wrangler CLI
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# 1. Install Wrangler CLI if you haven't already: npm install -g wrangler
|
||||||
|
# 2. Login to your Cloudflare account: wrangler login
|
||||||
|
# 3. Make sure that your wrangler.jsonc or wrangler.toml file has the following:
|
||||||
|
# [d1_databases]
|
||||||
|
# [d1_databases.DB]
|
||||||
|
# database_name = "YOUR_DB_NAME"
|
||||||
|
# database_id = "YOUR_DB_ID"
|
||||||
|
# 4. Replace YOUR_DB_NAME with your actual D1 database name
|
||||||
|
# 5. Replace YOUR_DB_ID with your actual D1 database ID
|
||||||
|
|
||||||
|
# Step 1: Write the query to a file
|
||||||
|
wrangler d1 execute YOUR_DB_NAME --command $'WITH fk_info AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', m.name, \\'column\\', fk.[from], \\'foreign_key_name\\', \\'fk_\\' || m.name || \\'_\\' || fk.[from] || \\'_\\' || fk.[table] || \\'_\\' || fk.[to], \\'reference_schema\\', \\'\\', \\'reference_table\\', fk.[table], \\'reference_column\\', fk.[to], \\'fk_def\\', \\'FOREIGN KEY (\\' || fk.[from] || \\') REFERENCES \\' || fk.[table] || \\'(\\' || fk.[to] || \\')\\' || \\' ON UPDATE \\' || fk.on_update || \\' ON DELETE \\' || fk.on_delete ) ) AS fk_metadata FROM sqlite_master m JOIN pragma_foreign_key_list(m.name) fk ON m.type = \\'table\\' WHERE m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ), pk_info AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', pk.table_name, \\'field_count\\', pk.field_count, \\'column\\', pk.pk_column, \\'pk_def\\', \\'PRIMARY KEY (\\' || pk.pk_column || \\')\\' ) ) AS pk_metadata FROM ( SELECT m.name AS table_name, COUNT(p.name) AS field_count, GROUP_CONCAT(p.name) AS pk_column FROM sqlite_master m JOIN pragma_table_info(m.name) p ON m.type = \\'table\\' AND p.pk > 0 WHERE m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' GROUP BY m.name ) pk ), indexes_metadata AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', m.name, \\'name\\', idx.name, \\'column\\', ic.name, \\'index_type\\', \\'B-TREE\\', \\'cardinality\\', \\'\\', \\'size\\', \\'\\', \\'unique\\', CASE WHEN idx.[unique] = 1 THEN \\'true\\' ELSE \\'false\\' END, \\'direction\\', \\'\\', \\'column_position\\', ic.seqno + 1 ) ) AS indexes_metadata FROM sqlite_master m JOIN pragma_index_list(m.name) idx ON m.type = \\'table\\' JOIN pragma_index_info(idx.name) ic WHERE m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ), cols AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', m.name, \\'name\\', p.name, \\'type\\', CASE WHEN INSTR(LOWER(p.type), \\'(\\') > 0 THEN SUBSTR(LOWER(p.type), 1, INSTR(LOWER(p.type), \\'(\\') - 1) ELSE LOWER(p.type) END, \\'ordinal_position\\', p.cid, \\'nullable\\', CASE WHEN p.[notnull] = 0 THEN true ELSE false END, \\'collation\\', \\'\\', \\'character_maximum_length\\', CASE WHEN LOWER(p.type) LIKE \\'char%\\' OR LOWER(p.type) LIKE \\'varchar%\\' THEN CASE WHEN INSTR(p.type, \\'(\\') > 0 THEN REPLACE( SUBSTR(p.type, INSTR(p.type, \\'(\\') + 1, LENGTH(p.type) - INSTR(p.type, \\'(\\') - 1), \\')\\', \\'\\' ) ELSE \\'null\\' END ELSE \\'null\\' END, \\'precision\\', CASE WHEN LOWER(p.type) LIKE \\'decimal%\\' OR LOWER(p.type) LIKE \\'numeric%\\' THEN CASE WHEN instr(p.type, \\'(\\') > 0 THEN json_object( \\'precision\\', substr(p.type, instr(p.type, \\'(\\') + 1, instr(p.type, \\',\\') - instr(p.type, \\'(\\') - 1), \\'scale\\', substr(p.type, instr(p.type, \\',\\') + 1, instr(p.type, \\')\\') - instr(p.type, \\',\\') - 1) ) ELSE \\'null\\' END ELSE \\'null\\' END, \\'default\\', COALESCE(REPLACE(p.dflt_value, \\'"\\', \\'\\\\\\"\\'), \\'\\') ) ) AS cols_metadata FROM sqlite_master m JOIN pragma_table_info(m.name) p ON m.type in (\\'table\\', \\'view\\') WHERE m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ), tbls AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', m.name, \\'rows\\', -1, \\'type\\', \\'table\\', \\'engine\\', \\'\\', \\'collation\\', \\'\\' ) ) AS tbls_metadata FROM sqlite_master m WHERE m.type in (\\'table\\', \\'view\\') AND m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ), views AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'view_name\\', m.name ) ) AS views_metadata FROM sqlite_master m WHERE m.type = \\'view\\' AND m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ) SELECT json_object( \\'fk_info\\', json((SELECT fk_metadata FROM fk_info)), \\'pk_info\\', json((SELECT pk_metadata FROM pk_info)), \\'columns\\', json((SELECT cols_metadata FROM cols)), \\'indexes\\', json((SELECT indexes_metadata FROM indexes_metadata)), \\'tables\\', json((SELECT tbls_metadata FROM tbls)), \\'views\\', json((SELECT views_metadata FROM views)), \\'database_name\\', \\'sqlite\\', \\'version\\', \\'\\' ) AS metadata_json_to_import;' --remote
|
||||||
|
|
||||||
|
# Step 2: Copy the output of the command above and paste it into app.chartdb.io
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSQLiteQuery = (
|
||||||
|
options: {
|
||||||
|
databaseEdition?: DatabaseEdition;
|
||||||
|
databaseClient?: DatabaseClient;
|
||||||
|
} = {}
|
||||||
|
): string => {
|
||||||
|
// For Cloudflare D1 edition, return the D1 script
|
||||||
|
if (options.databaseEdition === DatabaseEdition.SQLITE_CLOUDFLARE_D1) {
|
||||||
|
// Generate the Wrangler CLI command based on client
|
||||||
|
const isWranglerClient =
|
||||||
|
options?.databaseClient === DatabaseClient.SQLITE_WRANGLER;
|
||||||
|
|
||||||
|
if (isWranglerClient) {
|
||||||
|
return generateWranglerCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloudflareD1Query;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default SQLite script
|
||||||
|
return sqliteQuery;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
|
|
||||||
const sqlServerQuery = `WITH fk_info AS (
|
const sqlServerQuery = `${`/* SQL Server 2017 and above edition (14.0, 15.0, 16.0, 17.0)*/`}
|
||||||
|
WITH fk_info AS (
|
||||||
SELECT
|
SELECT
|
||||||
JSON_QUERY(
|
JSON_QUERY(
|
||||||
'[' + STRING_AGG(
|
N'[' + STRING_AGG(
|
||||||
CONVERT(nvarchar(max),
|
CONVERT(nvarchar(max),
|
||||||
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
JSON_QUERY(N'{
|
||||||
'", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
|
||||||
'", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
|
||||||
'", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
|
||||||
'", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
|
||||||
'", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
|
||||||
'", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
|
||||||
'", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
|
||||||
') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
|
||||||
'(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
|
||||||
') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
|
||||||
' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}')
|
') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
|
||||||
), ','
|
' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
|
||||||
|
'"}') COLLATE DATABASE_DEFAULT
|
||||||
|
), N','
|
||||||
) + N']'
|
) + N']'
|
||||||
) AS all_fks_json
|
) AS all_fks_json
|
||||||
FROM sys.foreign_keys AS fk
|
FROM sys.foreign_keys AS fk
|
||||||
@@ -31,299 +34,271 @@ const sqlServerQuery = `WITH fk_info AS (
|
|||||||
), pk_info AS (
|
), pk_info AS (
|
||||||
SELECT
|
SELECT
|
||||||
JSON_QUERY(
|
JSON_QUERY(
|
||||||
'[' + STRING_AGG(
|
N'[' +
|
||||||
CONVERT(nvarchar(max),
|
STRING_AGG(
|
||||||
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
CONVERT(nvarchar(max),
|
||||||
'", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
JSON_QUERY(N'{
|
||||||
'", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
|
||||||
'", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}')
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
|
||||||
), ','
|
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
|
||||||
) + N']'
|
'", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
|
||||||
|
), N','
|
||||||
|
) + N']'
|
||||||
) AS all_pks_json
|
) AS all_pks_json
|
||||||
FROM
|
FROM (
|
||||||
(
|
SELECT
|
||||||
SELECT
|
kcu.TABLE_SCHEMA,
|
||||||
kcu.TABLE_SCHEMA,
|
kcu.TABLE_NAME,
|
||||||
kcu.TABLE_NAME,
|
kcu.COLUMN_NAME
|
||||||
kcu.COLUMN_NAME
|
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
||||||
FROM
|
JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
||||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
||||||
JOIN
|
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
||||||
INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||||
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
) pk
|
||||||
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
|
||||||
WHERE
|
|
||||||
tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
|
||||||
) pk
|
|
||||||
),
|
),
|
||||||
cols AS (
|
cols AS (
|
||||||
SELECT
|
SELECT
|
||||||
JSON_QUERY(
|
JSON_QUERY(N'[' +
|
||||||
'[' + STRING_AGG(
|
STRING_AGG(
|
||||||
CONVERT(nvarchar(max),
|
CONVERT(nvarchar(max),
|
||||||
JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') +
|
JSON_QUERY(N'{
|
||||||
'", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
|
||||||
'", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') +
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
|
||||||
'", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
|
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
|
||||||
'", "type": "' + LOWER(cols.DATA_TYPE) +
|
'", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
|
||||||
'", "character_maximum_length": "' +
|
', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
|
||||||
COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') +
|
'", "character_maximum_length": ' +
|
||||||
'", "precision": ' +
|
CASE
|
||||||
CASE
|
WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
|
||||||
WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN
|
ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
|
||||||
CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'),
|
END +
|
||||||
',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
|
', "precision": ' +
|
||||||
ELSE
|
CASE
|
||||||
'null'
|
WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
|
||||||
END +
|
THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
|
||||||
', "nullable": ' +
|
',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
|
||||||
CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
ELSE 'null'
|
||||||
', "default": "' +
|
END +
|
||||||
COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), '') +
|
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
||||||
'", "collation": "' +
|
', "default": ' +
|
||||||
COALESCE(cols.COLLATION_NAME, '') +
|
'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
|
||||||
'"}')
|
', "collation": ' + CASE
|
||||||
), ','
|
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
|
||||||
) + ']'
|
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
|
||||||
) AS all_columns_json
|
END +
|
||||||
FROM
|
N'}') COLLATE DATABASE_DEFAULT
|
||||||
INFORMATION_SCHEMA.COLUMNS cols
|
), N','
|
||||||
WHERE
|
) +
|
||||||
cols.TABLE_CATALOG = DB_NAME()
|
N']') AS all_columns_json
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS cols
|
||||||
|
WHERE cols.TABLE_CATALOG = DB_NAME()
|
||||||
),
|
),
|
||||||
indexes AS (
|
indexes AS (
|
||||||
SELECT
|
SELECT
|
||||||
'[' + STRING_AGG(
|
N'[' +
|
||||||
CONVERT(nvarchar(max),
|
STRING_AGG(
|
||||||
JSON_QUERY(
|
CONVERT(nvarchar(max),
|
||||||
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
JSON_QUERY(N'{
|
||||||
'", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
|
||||||
'", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
|
||||||
'", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
|
||||||
'", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
|
||||||
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
|
'", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
|
||||||
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
|
||||||
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
|
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
|
||||||
)
|
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
|
||||||
), ','
|
) COLLATE DATABASE_DEFAULT
|
||||||
) + N']' AS all_indexes_json
|
), N','
|
||||||
FROM
|
) +
|
||||||
sys.indexes i
|
N']' AS all_indexes_json
|
||||||
JOIN
|
FROM sys.indexes i
|
||||||
sys.tables t ON i.object_id = t.object_id
|
JOIN sys.tables t ON i.object_id = t.object_id
|
||||||
JOIN
|
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||||
sys.schemas s ON t.schema_id = s.schema_id
|
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
||||||
JOIN
|
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||||
sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
WHERE s.name LIKE '%' AND i.name IS NOT NULL AND ic.is_included_column = 0
|
||||||
JOIN
|
|
||||||
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
||||||
WHERE
|
|
||||||
s.name LIKE '%'
|
|
||||||
AND i.name IS NOT NULL
|
|
||||||
),
|
),
|
||||||
tbls AS (
|
tbls AS (
|
||||||
SELECT
|
SELECT
|
||||||
'[' + STRING_AGG(
|
N'[' + STRING_AGG(
|
||||||
CONVERT(nvarchar(max),
|
CONVERT(nvarchar(max),
|
||||||
JSON_QUERY(
|
JSON_QUERY(N'{
|
||||||
N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
|
||||||
'", "table": "' + COALESCE(REPLACE(aggregated.table_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
|
||||||
'", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
|
'", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
|
||||||
'", "table_type": "' + aggregated.table_type COLLATE SQL_Latin1_General_CP1_CI_AS +
|
', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
|
||||||
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}'
|
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
|
||||||
)
|
) COLLATE DATABASE_DEFAULT
|
||||||
), ','
|
), N','
|
||||||
) + N']' AS all_tables_json
|
) +
|
||||||
FROM
|
N']' AS all_tables_json
|
||||||
(
|
FROM (
|
||||||
-- Select from tables
|
SELECT
|
||||||
SELECT
|
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
|
||||||
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
|
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
|
||||||
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
|
SUM(p.rows) AS row_count,
|
||||||
SUM(p.rows) AS row_count,
|
t.type_desc AS table_type,
|
||||||
t.type_desc AS table_type,
|
t.create_date AS creation_date
|
||||||
t.create_date AS creation_date
|
FROM sys.tables t
|
||||||
FROM
|
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||||
sys.tables t
|
JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
|
||||||
JOIN
|
WHERE s.name LIKE '%'
|
||||||
sys.schemas s ON t.schema_id = s.schema_id
|
GROUP BY s.name, t.name, t.type_desc, t.create_date
|
||||||
JOIN
|
|
||||||
sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
|
|
||||||
WHERE
|
|
||||||
s.name LIKE '%'
|
|
||||||
GROUP BY
|
|
||||||
s.name, t.name, t.type_desc, t.create_date
|
|
||||||
|
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
-- Select from views
|
SELECT
|
||||||
SELECT
|
COALESCE(REPLACE(s.name, '"', ''), '') AS table_name,
|
||||||
COALESCE(REPLACE(s.name, '"', ''), '') AS table_name,
|
COALESCE(REPLACE(v.name, '"', ''), '') AS object_name,
|
||||||
COALESCE(REPLACE(v.name, '"', ''), '') AS object_name,
|
0 AS row_count,
|
||||||
0 AS row_count, -- Views don't have row counts
|
'VIEW' AS table_type,
|
||||||
'VIEW' AS table_type,
|
v.create_date AS creation_date
|
||||||
v.create_date AS creation_date
|
FROM sys.views v
|
||||||
FROM
|
JOIN sys.schemas s ON v.schema_id = s.schema_id
|
||||||
sys.views v
|
WHERE s.name LIKE '%'
|
||||||
JOIN
|
) AS aggregated
|
||||||
sys.schemas s ON v.schema_id = s.schema_id
|
|
||||||
WHERE
|
|
||||||
s.name LIKE '%'
|
|
||||||
) AS aggregated
|
|
||||||
),
|
),
|
||||||
views AS (
|
views AS (
|
||||||
SELECT
|
SELECT
|
||||||
'[' + STRING_AGG(
|
'[' + STRING_AGG(
|
||||||
CONVERT(nvarchar(max),
|
CONVERT(nvarchar(max),
|
||||||
JSON_QUERY(
|
JSON_QUERY(N'{
|
||||||
N'{"schema": "' + STRING_ESCAPE(COALESCE(s.name, ''), 'json') +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
|
||||||
'", "view_name": "' + STRING_ESCAPE(COALESCE(v.name, ''), 'json') +
|
'", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
|
||||||
'", "view_definition": "' +
|
'", "view_definition": "' +
|
||||||
STRING_ESCAPE(
|
STRING_ESCAPE(
|
||||||
CAST(
|
CAST(
|
||||||
'' AS XML
|
'' AS XML
|
||||||
).value(
|
).value(
|
||||||
'xs:base64Binary(sql:column("DefinitionBinary"))',
|
'xs:base64Binary(sql:column("DefinitionBinary"))',
|
||||||
'VARCHAR(MAX)'
|
'VARCHAR(MAX)'
|
||||||
), 'json') +
|
), 'json') +
|
||||||
'"}'
|
N'"}') COLLATE DATABASE_DEFAULT
|
||||||
)
|
), N','
|
||||||
), ','
|
|
||||||
) + N']' AS all_views_json
|
) + N']' AS all_views_json
|
||||||
FROM
|
FROM sys.views v
|
||||||
sys.views v
|
JOIN sys.schemas s ON v.schema_id = s.schema_id
|
||||||
JOIN
|
JOIN sys.sql_modules m ON v.object_id = m.object_id
|
||||||
sys.schemas s ON v.schema_id = s.schema_id
|
|
||||||
JOIN
|
|
||||||
sys.sql_modules m ON v.object_id = m.object_id
|
|
||||||
CROSS APPLY
|
CROSS APPLY
|
||||||
(SELECT CONVERT(VARBINARY(MAX), m.definition) AS DefinitionBinary) AS bin
|
(SELECT CONVERT(VARBINARY(MAX), m.definition) AS DefinitionBinary) AS bin
|
||||||
WHERE
|
WHERE s.name LIKE '%'
|
||||||
s.name LIKE '%'
|
|
||||||
)
|
)
|
||||||
SELECT JSON_QUERY(
|
SELECT JSON_QUERY(
|
||||||
N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
|
N'{
|
||||||
|
"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
|
||||||
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
|
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
|
||||||
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
|
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
|
||||||
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
|
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
|
||||||
', "tables": ' + ISNULL((SELECT cast(all_tables_json as nvarchar(max)) FROM tbls), N'[]') +
|
', "tables": ' + ISNULL((SELECT cast(all_tables_json as nvarchar(max)) FROM tbls), N'[]') +
|
||||||
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
|
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
|
||||||
', "database_name": "' + DB_NAME() + '"' +
|
', "database_name": "' + STRING_ESCAPE(DB_NAME(), 'json') +
|
||||||
', "version": ""}'
|
'", "version": ""
|
||||||
|
}'
|
||||||
) AS metadata_json_to_import;
|
) AS metadata_json_to_import;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const sqlServer2016AndBelowQuery = `WITH fk_info AS (
|
const sqlServer2016AndBelowQuery = `${`/* SQL Server 2016 and below edition (13.0, 12.0, 11.0..) */`}
|
||||||
SELECT
|
WITH fk_info AS (
|
||||||
JSON_QUERY(
|
SELECT JSON_QUERY('[' +
|
||||||
'[' + ISNULL(
|
ISNULL(
|
||||||
STUFF((
|
STUFF((
|
||||||
SELECT ',' +
|
SELECT ',' +
|
||||||
CONVERT(nvarchar(max),
|
CONVERT(nvarchar(max),
|
||||||
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
JSON_QUERY(N'{
|
||||||
'", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
|
||||||
'", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
|
||||||
'", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
|
||||||
'", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
|
||||||
'", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
|
||||||
'", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
|
||||||
'", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
|
||||||
') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
|
||||||
'(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
|
||||||
') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
|
||||||
' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}')
|
') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
|
||||||
)
|
' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
|
||||||
FROM
|
'"}') COLLATE DATABASE_DEFAULT
|
||||||
sys.foreign_keys AS fk
|
)
|
||||||
JOIN
|
FROM sys.foreign_keys AS fk
|
||||||
sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
|
JOIN sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
|
||||||
JOIN
|
JOIN sys.tables AS tp ON fkc.parent_object_id = tp.object_id
|
||||||
sys.tables AS tp ON fkc.parent_object_id = tp.object_id
|
JOIN sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id
|
||||||
JOIN
|
JOIN sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
|
||||||
sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id
|
JOIN sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
|
||||||
JOIN
|
JOIN sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
|
||||||
sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
|
JOIN sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
|
||||||
JOIN
|
FOR XML PATH('')
|
||||||
sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
|
), 1, 1, ''), '')
|
||||||
JOIN
|
+ N']') AS all_fks_json
|
||||||
sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
|
|
||||||
JOIN
|
|
||||||
sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
|
|
||||||
FOR XML PATH('')
|
|
||||||
), 1, 1, ''), '')
|
|
||||||
+ N']'
|
|
||||||
) AS all_fks_json
|
|
||||||
),
|
),
|
||||||
pk_info AS (
|
pk_info AS (
|
||||||
SELECT
|
SELECT JSON_QUERY('[' +
|
||||||
JSON_QUERY(
|
ISNULL(STUFF((
|
||||||
'[' + ISNULL(
|
|
||||||
STUFF((
|
|
||||||
SELECT ',' +
|
SELECT ',' +
|
||||||
CONVERT(nvarchar(max),
|
CONVERT(nvarchar(max),
|
||||||
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
JSON_QUERY(N'{
|
||||||
'", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
|
||||||
'", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
|
||||||
'", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}')
|
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
|
||||||
|
'", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
|
||||||
)
|
)
|
||||||
FROM
|
FROM
|
||||||
(
|
(
|
||||||
SELECT
|
SELECT kcu.TABLE_SCHEMA,
|
||||||
kcu.TABLE_SCHEMA,
|
kcu.TABLE_NAME,
|
||||||
kcu.TABLE_NAME,
|
kcu.COLUMN_NAME
|
||||||
kcu.COLUMN_NAME
|
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
||||||
FROM
|
JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
||||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
|
||||||
JOIN
|
|
||||||
INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
||||||
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
||||||
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
||||||
WHERE
|
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||||
tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
|
||||||
) pk
|
) pk
|
||||||
FOR XML PATH('')
|
FOR XML PATH('')
|
||||||
), 1, 1, ''), '')
|
), 1, 1, ''), '')
|
||||||
+ N']'
|
+ N']') AS all_pks_json
|
||||||
) AS all_pks_json
|
|
||||||
),
|
),
|
||||||
cols AS (
|
cols AS (
|
||||||
SELECT
|
SELECT JSON_QUERY('[' +
|
||||||
JSON_QUERY(
|
ISNULL(
|
||||||
'[' + ISNULL(
|
STUFF((
|
||||||
STUFF((
|
SELECT ',' +
|
||||||
SELECT ',' +
|
CONVERT(nvarchar(max),
|
||||||
CONVERT(nvarchar(max),
|
JSON_QUERY('{
|
||||||
JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
|
||||||
'", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') +
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
|
||||||
'", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') +
|
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
|
||||||
'", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
|
'", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
|
||||||
'", "type": "' + LOWER(cols.DATA_TYPE) +
|
', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
|
||||||
'", "character_maximum_length": "' +
|
'", "character_maximum_length": ' +
|
||||||
COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') +
|
CASE
|
||||||
'", "precision": ' +
|
WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
|
||||||
CASE
|
ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
|
||||||
WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN
|
END +
|
||||||
CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'),
|
', "precision": ' +
|
||||||
',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
|
CASE
|
||||||
ELSE
|
WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
|
||||||
'null'
|
THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
|
||||||
END +
|
',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
|
||||||
', "nullable": ' +
|
ELSE 'null'
|
||||||
CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
END +
|
||||||
', "default": "' +
|
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
||||||
COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '"'), '') +
|
', "default": ' +
|
||||||
'", "collation": "' +
|
'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
|
||||||
COALESCE(cols.COLLATION_NAME, '') +
|
', "collation": ' +
|
||||||
'"}')
|
CASE
|
||||||
)
|
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
|
||||||
FROM
|
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
|
||||||
INFORMATION_SCHEMA.COLUMNS cols
|
END +
|
||||||
WHERE
|
N'}')
|
||||||
cols.TABLE_CATALOG = DB_NAME()
|
)
|
||||||
FOR XML PATH('')
|
FROM
|
||||||
), 1, 1, ''), '')
|
INFORMATION_SCHEMA.COLUMNS cols
|
||||||
+ ']'
|
WHERE
|
||||||
) AS all_columns_json
|
cols.TABLE_CATALOG = DB_NAME()
|
||||||
|
FOR XML PATH('')
|
||||||
|
), 1, 1, ''), '')
|
||||||
|
+ ']') AS all_columns_json
|
||||||
),
|
),
|
||||||
indexes AS (
|
indexes AS (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -331,30 +306,25 @@ indexes AS (
|
|||||||
STUFF((
|
STUFF((
|
||||||
SELECT ',' +
|
SELECT ',' +
|
||||||
CONVERT(nvarchar(max),
|
CONVERT(nvarchar(max),
|
||||||
JSON_QUERY(
|
JSON_QUERY(N'{
|
||||||
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
|
||||||
'", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
|
||||||
'", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
|
||||||
'", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
|
||||||
'", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
|
||||||
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
|
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
|
||||||
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS +
|
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
|
||||||
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
|
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
|
||||||
)
|
) COLLATE DATABASE_DEFAULT
|
||||||
)
|
)
|
||||||
FROM
|
FROM sys.indexes i
|
||||||
sys.indexes i
|
JOIN sys.tables t ON i.object_id = t.object_id
|
||||||
JOIN
|
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||||
sys.tables t ON i.object_id = t.object_id
|
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
||||||
JOIN
|
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||||
sys.schemas s ON t.schema_id = s.schema_id
|
WHERE s.name LIKE '%'
|
||||||
JOIN
|
AND i.name IS NOT NULL
|
||||||
sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
AND ic.is_included_column = 0
|
||||||
JOIN
|
|
||||||
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
||||||
WHERE
|
|
||||||
s.name LIKE '%'
|
|
||||||
AND i.name IS NOT NULL
|
|
||||||
FOR XML PATH('')
|
FOR XML PATH('')
|
||||||
), 1, 1, ''), '')
|
), 1, 1, ''), '')
|
||||||
+ N']' AS all_indexes_json
|
+ N']' AS all_indexes_json
|
||||||
@@ -365,12 +335,12 @@ tbls AS (
|
|||||||
STUFF((
|
STUFF((
|
||||||
SELECT ',' +
|
SELECT ',' +
|
||||||
CONVERT(nvarchar(max),
|
CONVERT(nvarchar(max),
|
||||||
JSON_QUERY(
|
JSON_QUERY(N'{
|
||||||
N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
|
||||||
'", "table": "' + COALESCE(REPLACE(aggregated.object_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
|
||||||
'", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
|
'", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
|
||||||
'", "object_type": "' + aggregated.object_type COLLATE SQL_Latin1_General_CP1_CI_AS +
|
', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
|
||||||
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}'
|
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
FROM
|
FROM
|
||||||
@@ -378,20 +348,15 @@ tbls AS (
|
|||||||
-- Select from tables
|
-- Select from tables
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
|
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
|
||||||
COALESCE(REPLACE(t.name, '"', ''), '') AS object_name,
|
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
|
||||||
SUM(p.rows) AS row_count,
|
SUM(p.rows) AS row_count,
|
||||||
t.type_desc AS object_type,
|
t.type_desc AS table_type,
|
||||||
t.create_date AS creation_date
|
t.create_date AS creation_date
|
||||||
FROM
|
FROM sys.tables t
|
||||||
sys.tables t
|
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||||
JOIN
|
JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
|
||||||
sys.schemas s ON t.schema_id = s.schema_id
|
WHERE s.name LIKE '%'
|
||||||
JOIN
|
GROUP BY s.name, t.name, t.type_desc, t.create_date
|
||||||
sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
|
|
||||||
WHERE
|
|
||||||
s.name LIKE '%'
|
|
||||||
GROUP BY
|
|
||||||
s.name, t.name, t.type_desc, t.create_date
|
|
||||||
|
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
@@ -402,12 +367,9 @@ tbls AS (
|
|||||||
0 AS row_count, -- Views don't have row counts
|
0 AS row_count, -- Views don't have row counts
|
||||||
'VIEW' AS object_type,
|
'VIEW' AS object_type,
|
||||||
v.create_date AS creation_date
|
v.create_date AS creation_date
|
||||||
FROM
|
FROM sys.views v
|
||||||
sys.views v
|
JOIN sys.schemas s ON v.schema_id = s.schema_id
|
||||||
JOIN
|
WHERE s.name LIKE '%'
|
||||||
sys.schemas s ON v.schema_id = s.schema_id
|
|
||||||
WHERE
|
|
||||||
s.name LIKE '%'
|
|
||||||
) AS aggregated
|
) AS aggregated
|
||||||
FOR XML PATH('')
|
FOR XML PATH('')
|
||||||
), 1, 1, ''), '')
|
), 1, 1, ''), '')
|
||||||
@@ -417,38 +379,40 @@ views AS (
|
|||||||
SELECT
|
SELECT
|
||||||
'[' +
|
'[' +
|
||||||
(
|
(
|
||||||
SELECT
|
SELECT STUFF((
|
||||||
STUFF((
|
SELECT ',' + CONVERT(nvarchar(max),
|
||||||
SELECT ',' + CONVERT(nvarchar(max),
|
JSON_QUERY(
|
||||||
JSON_QUERY(
|
N'{
|
||||||
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') +
|
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
|
||||||
'", "view_name": "' + COALESCE(REPLACE(v.name, '"', ''), '') +
|
'", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
|
||||||
'", "view_definition": "' +
|
'", "view_definition": "' +
|
||||||
CAST(
|
CAST(
|
||||||
(
|
(
|
||||||
SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
|
SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
|
||||||
) AS NVARCHAR(MAX)
|
) AS NVARCHAR(MAX)
|
||||||
) + '"}'
|
) + N'"}'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
FROM
|
||||||
FROM
|
sys.views v
|
||||||
sys.views v
|
JOIN
|
||||||
JOIN
|
sys.schemas s ON v.schema_id = s.schema_id
|
||||||
sys.schemas s ON v.schema_id = s.schema_id
|
WHERE
|
||||||
WHERE
|
s.name LIKE '%'
|
||||||
s.name LIKE '%'
|
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
|
||||||
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
|
|
||||||
) + ']' AS all_views_json
|
) + ']' AS all_views_json
|
||||||
)
|
)
|
||||||
SELECT JSON_QUERY(
|
SELECT JSON_QUERY(
|
||||||
N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
|
N'{
|
||||||
|
"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
|
||||||
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
|
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
|
||||||
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
|
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
|
||||||
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
|
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
|
||||||
', "tables": ' + ISNULL((SELECT cast(all_objects_json as nvarchar(max)) FROM tbls), N'[]') +
|
', "tables": ' + ISNULL((SELECT cast(all_objects_json as nvarchar(max)) FROM tbls), N'[]') +
|
||||||
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
|
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
|
||||||
', "database_name": "' + DB_NAME() + '"' +
|
', "database_name": "' + DB_NAME() + '"' +
|
||||||
', "version": ""}'
|
', "version": ""
|
||||||
|
}'
|
||||||
) AS metadata_json_to_import;`;
|
) AS metadata_json_to_import;`;
|
||||||
|
|
||||||
export const getSqlServerQuery = (
|
export const getSqlServerQuery = (
|
||||||
|
|||||||
@@ -10,14 +10,20 @@ export const fixMetadataJson = async (
|
|||||||
return (
|
return (
|
||||||
metadataJson
|
metadataJson
|
||||||
.trim()
|
.trim()
|
||||||
|
// First unescape the JSON string
|
||||||
|
.replace(/\\"/g, '"')
|
||||||
|
.replace(/\\\\/g, '\\')
|
||||||
.replace(/^[^{]*/, '') // Remove everything before the first '{'
|
.replace(/^[^{]*/, '') // Remove everything before the first '{'
|
||||||
.replace(/}[^}]*$/, '}') // Remove everything after the last '}'
|
.replace(/}[^}]*$/, '}') // Remove everything after the last '}'
|
||||||
|
.replace(/:""([^"]+)""/g, ':"$1"') // Convert :""value"" to :"value"
|
||||||
|
.replace(/""(\w+)""/g, '"$1"') // Convert ""key"" to "key"
|
||||||
.replace(/^\s+|\s+$/g, '')
|
.replace(/^\s+|\s+$/g, '')
|
||||||
.replace(/^"|"$/g, '')
|
.replace(/^"|"$/g, '')
|
||||||
.replace(/^'|'$/g, '')
|
.replace(/^'|'$/g, '')
|
||||||
.replace(/""""/g, '""') // Remove Quadruple quotes from keys
|
.replace(/""""/g, '""') // Remove Quadruple quotes from keys
|
||||||
.replace(/"""([^",}]+)"""/g, '"$1"') // Remove tripple quotes from keys
|
.replace(/"""([^",}]+)"""/g, '"$1"') // Remove tripple quotes from keys
|
||||||
.replace(/""([^",}]+)""/g, '"$1"') // Remove double quotes from keys
|
.replace(/""([^",}]+)""/g, '"$1"') // Remove double quotes from keys
|
||||||
|
|
||||||
/* eslint-disable-next-line no-useless-escape */
|
/* eslint-disable-next-line no-useless-escape */
|
||||||
.replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
|
.replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
|
||||||
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
|
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
|
||||||
|
|||||||
304
src/lib/dbml-import.ts
Normal file
304
src/lib/dbml-import.ts
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import { Parser } from '@dbml/core';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import { generateDiagramId, generateId } from '@/lib/utils';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
|
||||||
|
import { randomColor } from '@/lib/colors';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
|
||||||
|
interface DBMLTypeArgs {
|
||||||
|
length?: number;
|
||||||
|
precision?: number;
|
||||||
|
scale?: number;
|
||||||
|
values?: string[]; // For enum types
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLField {
|
||||||
|
name: string;
|
||||||
|
type: {
|
||||||
|
type_name: string;
|
||||||
|
args?: DBMLTypeArgs;
|
||||||
|
};
|
||||||
|
unique?: boolean;
|
||||||
|
pk?: boolean;
|
||||||
|
not_null?: boolean;
|
||||||
|
increment?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLIndexColumn {
|
||||||
|
value: string;
|
||||||
|
type?: string;
|
||||||
|
length?: number;
|
||||||
|
order?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLIndex {
|
||||||
|
columns: string | (string | DBMLIndexColumn)[];
|
||||||
|
unique?: boolean;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLTable {
|
||||||
|
name: string;
|
||||||
|
schema?: string | { name: string };
|
||||||
|
fields: DBMLField[];
|
||||||
|
indexes?: DBMLIndex[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLEndpoint {
|
||||||
|
tableName: string;
|
||||||
|
fieldNames: string[];
|
||||||
|
relation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLRef {
|
||||||
|
endpoints: [DBMLEndpoint, DBMLEndpoint];
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDBMLTypeToGenericType = (dbmlType: string): DataType => {
|
||||||
|
const normalizedType = dbmlType.toLowerCase().replace(/\(.*\)/, '');
|
||||||
|
const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
|
||||||
|
if (matchedType) return matchedType;
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
int: 'integer',
|
||||||
|
varchar: 'varchar',
|
||||||
|
bool: 'boolean',
|
||||||
|
number: 'numeric',
|
||||||
|
string: 'varchar',
|
||||||
|
text: 'text',
|
||||||
|
timestamp: 'timestamp',
|
||||||
|
datetime: 'timestamp',
|
||||||
|
float: 'float',
|
||||||
|
double: 'double',
|
||||||
|
decimal: 'decimal',
|
||||||
|
bigint: 'bigint',
|
||||||
|
smallint: 'smallint',
|
||||||
|
char: 'char',
|
||||||
|
};
|
||||||
|
const mappedType = typeMap[normalizedType];
|
||||||
|
if (mappedType) {
|
||||||
|
const foundType = genericDataTypes.find((t) => t.id === mappedType);
|
||||||
|
if (foundType) return foundType;
|
||||||
|
}
|
||||||
|
return genericDataTypes.find((t) => t.id === 'varchar')!;
|
||||||
|
};
|
||||||
|
|
||||||
|
const determineCardinality = (
|
||||||
|
field: DBField,
|
||||||
|
referencedField: DBField
|
||||||
|
): { sourceCardinality: string; targetCardinality: string } => {
|
||||||
|
const isSourceUnique = field.unique || field.primaryKey;
|
||||||
|
const isTargetUnique = referencedField.unique || referencedField.primaryKey;
|
||||||
|
if (isSourceUnique && isTargetUnique) {
|
||||||
|
return { sourceCardinality: 'one', targetCardinality: 'one' };
|
||||||
|
} else if (isSourceUnique) {
|
||||||
|
return { sourceCardinality: 'one', targetCardinality: 'many' };
|
||||||
|
} else if (isTargetUnique) {
|
||||||
|
return { sourceCardinality: 'many', targetCardinality: 'one' };
|
||||||
|
} else {
|
||||||
|
return { sourceCardinality: 'many', targetCardinality: 'many' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importDBMLToDiagram = async (
|
||||||
|
dbmlContent: string
|
||||||
|
): Promise<Diagram> => {
|
||||||
|
try {
|
||||||
|
const parser = new Parser();
|
||||||
|
const parsedData = parser.parse(dbmlContent, 'dbml');
|
||||||
|
const dbmlData = parsedData.schemas[0];
|
||||||
|
|
||||||
|
// Extract only the necessary data from the parsed DBML
|
||||||
|
const extractedData = {
|
||||||
|
tables: (dbmlData.tables as unknown as DBMLTable[]).map(
|
||||||
|
(table) => ({
|
||||||
|
name: table.name,
|
||||||
|
schema: table.schema,
|
||||||
|
fields: table.fields.map((field: DBMLField) => ({
|
||||||
|
name: field.name,
|
||||||
|
type: field.type,
|
||||||
|
unique: field.unique,
|
||||||
|
pk: field.pk,
|
||||||
|
not_null: field.not_null,
|
||||||
|
increment: field.increment,
|
||||||
|
})),
|
||||||
|
indexes:
|
||||||
|
table.indexes?.map((dbmlIndex) => {
|
||||||
|
let indexColumns: string[];
|
||||||
|
|
||||||
|
// Handle composite index case "(col1, col2)"
|
||||||
|
if (typeof dbmlIndex.columns === 'string') {
|
||||||
|
if (dbmlIndex.columns.includes('(')) {
|
||||||
|
// Composite index
|
||||||
|
const columnsStr =
|
||||||
|
dbmlIndex.columns.replace(/[()]/g, '');
|
||||||
|
indexColumns = columnsStr
|
||||||
|
.split(',')
|
||||||
|
.map((c) => c.trim());
|
||||||
|
} else {
|
||||||
|
// Single column
|
||||||
|
indexColumns = [dbmlIndex.columns.trim()];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle array of columns
|
||||||
|
indexColumns = Array.isArray(dbmlIndex.columns)
|
||||||
|
? dbmlIndex.columns.map((col) =>
|
||||||
|
typeof col === 'object' &&
|
||||||
|
'value' in col
|
||||||
|
? (col.value as string).trim()
|
||||||
|
: (col as string).trim()
|
||||||
|
)
|
||||||
|
: [String(dbmlIndex.columns).trim()];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a consistent index name
|
||||||
|
const indexName =
|
||||||
|
dbmlIndex.name ||
|
||||||
|
`idx_${table.name}_${indexColumns.join('_')}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns: indexColumns,
|
||||||
|
unique: dbmlIndex.unique || false,
|
||||||
|
name: indexName,
|
||||||
|
};
|
||||||
|
}) || [],
|
||||||
|
})
|
||||||
|
),
|
||||||
|
refs: (dbmlData.refs as unknown as DBMLRef[]).map((ref) => ({
|
||||||
|
endpoints: (ref.endpoints as [DBMLEndpoint, DBMLEndpoint]).map(
|
||||||
|
(endpoint) => ({
|
||||||
|
tableName: endpoint.tableName,
|
||||||
|
fieldNames: endpoint.fieldNames,
|
||||||
|
relation: endpoint.relation,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert DBML tables to ChartDB table objects
|
||||||
|
const tables: DBTable[] = extractedData.tables.map((table, index) => {
|
||||||
|
const row = Math.floor(index / 4);
|
||||||
|
const col = index % 4;
|
||||||
|
const tableSpacing = 300;
|
||||||
|
|
||||||
|
// Create fields first so we have their IDs
|
||||||
|
const fields = table.fields.map((field) => ({
|
||||||
|
id: generateId(),
|
||||||
|
name: field.name.replace(/['"]/g, ''),
|
||||||
|
type: mapDBMLTypeToGenericType(field.type.type_name),
|
||||||
|
nullable: !field.not_null,
|
||||||
|
primaryKey: field.pk || false,
|
||||||
|
unique: field.unique || false,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Convert DBML indexes to ChartDB indexes
|
||||||
|
const indexes =
|
||||||
|
table.indexes?.map((dbmlIndex) => {
|
||||||
|
const fieldIds = dbmlIndex.columns.map((columnName) => {
|
||||||
|
const field = fields.find((f) => f.name === columnName);
|
||||||
|
if (!field) {
|
||||||
|
throw new Error(
|
||||||
|
`Index references non-existent column: ${columnName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return field.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: generateId(),
|
||||||
|
name:
|
||||||
|
dbmlIndex.name ||
|
||||||
|
`idx_${table.name}_${dbmlIndex.columns.join('_')}`,
|
||||||
|
fieldIds,
|
||||||
|
unique: dbmlIndex.unique || false,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: generateId(),
|
||||||
|
name: table.name.replace(/['"]/g, ''),
|
||||||
|
schema:
|
||||||
|
typeof table.schema === 'string'
|
||||||
|
? table.schema
|
||||||
|
: table.schema?.name || '',
|
||||||
|
order: index,
|
||||||
|
fields,
|
||||||
|
indexes,
|
||||||
|
x: col * tableSpacing,
|
||||||
|
y: row * tableSpacing,
|
||||||
|
color: randomColor(),
|
||||||
|
isView: false,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create relationships using the refs
|
||||||
|
const relationships: DBRelationship[] = extractedData.refs.map(
|
||||||
|
(ref) => {
|
||||||
|
const [source, target] = ref.endpoints;
|
||||||
|
const sourceTable = tables.find(
|
||||||
|
(t) =>
|
||||||
|
t.name === source.tableName.replace(/['"]/g, '') &&
|
||||||
|
(!source.tableName.includes('.') ||
|
||||||
|
t.schema === source.tableName.split('.')[0])
|
||||||
|
);
|
||||||
|
const targetTable = tables.find(
|
||||||
|
(t) =>
|
||||||
|
t.name === target.tableName.replace(/['"]/g, '') &&
|
||||||
|
(!target.tableName.includes('.') ||
|
||||||
|
t.schema === target.tableName.split('.')[0])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceTable || !targetTable) {
|
||||||
|
throw new Error('Invalid relationship: tables not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceField = sourceTable.fields.find(
|
||||||
|
(f) => f.name === source.fieldNames[0].replace(/['"]/g, '')
|
||||||
|
);
|
||||||
|
const targetField = targetTable.fields.find(
|
||||||
|
(f) => f.name === target.fieldNames[0].replace(/['"]/g, '')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceField || !targetField) {
|
||||||
|
throw new Error('Invalid relationship: fields not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sourceCardinality, targetCardinality } =
|
||||||
|
determineCardinality(sourceField, targetField);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: generateId(),
|
||||||
|
name: `${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`,
|
||||||
|
sourceSchema: sourceTable.schema,
|
||||||
|
targetSchema: targetTable.schema,
|
||||||
|
sourceTableId: sourceTable.id,
|
||||||
|
targetTableId: targetTable.id,
|
||||||
|
sourceFieldId: sourceField.id,
|
||||||
|
targetFieldId: targetField.id,
|
||||||
|
sourceCardinality: sourceCardinality as Cardinality,
|
||||||
|
targetCardinality: targetCardinality as Cardinality,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: generateDiagramId(),
|
||||||
|
name: 'DBML Import',
|
||||||
|
databaseType: DatabaseType.GENERIC,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DBML parsing error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
import { DatabaseType } from './database-type';
|
import { DatabaseType } from './database-type';
|
||||||
|
import { DatabaseEdition } from './database-edition';
|
||||||
|
|
||||||
export enum DatabaseClient {
|
export enum DatabaseClient {
|
||||||
// PostgreSQL
|
// PostgreSQL
|
||||||
POSTGRESQL_PSQL = 'psql',
|
POSTGRESQL_PSQL = 'psql',
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
SQLITE_WRANGLER = 'wrangler',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const databaseClientToLabelMap: Record<DatabaseClient, string> = {
|
export const databaseClientToLabelMap: Record<DatabaseClient, string> = {
|
||||||
// PostgreSQL
|
// PostgreSQL
|
||||||
[DatabaseClient.POSTGRESQL_PSQL]: 'PSQL',
|
[DatabaseClient.POSTGRESQL_PSQL]: 'PSQL',
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
[DatabaseClient.SQLITE_WRANGLER]: 'Wrangler',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const databaseTypeToClientsMap: Record<DatabaseType, DatabaseClient[]> =
|
export const databaseTypeToClientsMap: Record<DatabaseType, DatabaseClient[]> =
|
||||||
@@ -21,3 +28,21 @@ export const databaseTypeToClientsMap: Record<DatabaseType, DatabaseClient[]> =
|
|||||||
[DatabaseType.CLICKHOUSE]: [],
|
[DatabaseType.CLICKHOUSE]: [],
|
||||||
[DatabaseType.COCKROACHDB]: [],
|
[DatabaseType.COCKROACHDB]: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const databaseEditionToClientsMap: Record<
|
||||||
|
DatabaseEdition,
|
||||||
|
DatabaseClient[]
|
||||||
|
> = {
|
||||||
|
// PostgreSQL
|
||||||
|
[DatabaseEdition.POSTGRESQL_SUPABASE]: [],
|
||||||
|
[DatabaseEdition.POSTGRESQL_TIMESCALE]: [],
|
||||||
|
|
||||||
|
// MySQL
|
||||||
|
[DatabaseEdition.MYSQL_5_7]: [],
|
||||||
|
|
||||||
|
// SQL Server
|
||||||
|
[DatabaseEdition.SQL_SERVER_2016_AND_BELOW]: [],
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
[DatabaseEdition.SQLITE_CLOUDFLARE_D1]: [DatabaseClient.SQLITE_WRANGLER],
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import SupabaseImage from '@/assets/supabase.png';
|
|||||||
import TimescaleImage from '@/assets/timescale.png';
|
import TimescaleImage from '@/assets/timescale.png';
|
||||||
import MySql5_7Image from '@/assets/mysql_5_7.png';
|
import MySql5_7Image from '@/assets/mysql_5_7.png';
|
||||||
import SqlServerImage from '@/assets/sql_server_logo_2.png';
|
import SqlServerImage from '@/assets/sql_server_logo_2.png';
|
||||||
|
import CloudflareD1Image from '@/assets/cloudflare_d1.png';
|
||||||
|
|
||||||
export enum DatabaseEdition {
|
export enum DatabaseEdition {
|
||||||
// PostgreSQL
|
// PostgreSQL
|
||||||
@@ -14,6 +15,9 @@ export enum DatabaseEdition {
|
|||||||
|
|
||||||
// SQL Server
|
// SQL Server
|
||||||
SQL_SERVER_2016_AND_BELOW = 'sql_server_2016_and_below',
|
SQL_SERVER_2016_AND_BELOW = 'sql_server_2016_and_below',
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
SQLITE_CLOUDFLARE_D1 = 'cloudflare_d1',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const databaseEditionToLabelMap: Record<DatabaseEdition, string> = {
|
export const databaseEditionToLabelMap: Record<DatabaseEdition, string> = {
|
||||||
@@ -26,6 +30,9 @@ export const databaseEditionToLabelMap: Record<DatabaseEdition, string> = {
|
|||||||
|
|
||||||
// SQL Server
|
// SQL Server
|
||||||
[DatabaseEdition.SQL_SERVER_2016_AND_BELOW]: '2016 and below',
|
[DatabaseEdition.SQL_SERVER_2016_AND_BELOW]: '2016 and below',
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
[DatabaseEdition.SQLITE_CLOUDFLARE_D1]: 'Cloudflare D1',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const databaseEditionToImageMap: Record<DatabaseEdition, string> = {
|
export const databaseEditionToImageMap: Record<DatabaseEdition, string> = {
|
||||||
@@ -38,6 +45,9 @@ export const databaseEditionToImageMap: Record<DatabaseEdition, string> = {
|
|||||||
|
|
||||||
// SQL Server
|
// SQL Server
|
||||||
[DatabaseEdition.SQL_SERVER_2016_AND_BELOW]: SqlServerImage,
|
[DatabaseEdition.SQL_SERVER_2016_AND_BELOW]: SqlServerImage,
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
[DatabaseEdition.SQLITE_CLOUDFLARE_D1]: CloudflareD1Image,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const databaseTypeToEditionMap: Record<DatabaseType, DatabaseEdition[]> =
|
export const databaseTypeToEditionMap: Record<DatabaseType, DatabaseEdition[]> =
|
||||||
@@ -48,7 +58,7 @@ export const databaseTypeToEditionMap: Record<DatabaseType, DatabaseEdition[]> =
|
|||||||
],
|
],
|
||||||
[DatabaseType.MYSQL]: [DatabaseEdition.MYSQL_5_7],
|
[DatabaseType.MYSQL]: [DatabaseEdition.MYSQL_5_7],
|
||||||
[DatabaseType.SQL_SERVER]: [DatabaseEdition.SQL_SERVER_2016_AND_BELOW],
|
[DatabaseType.SQL_SERVER]: [DatabaseEdition.SQL_SERVER_2016_AND_BELOW],
|
||||||
[DatabaseType.SQLITE]: [],
|
[DatabaseType.SQLITE]: [DatabaseEdition.SQLITE_CLOUDFLARE_D1],
|
||||||
[DatabaseType.GENERIC]: [],
|
[DatabaseType.GENERIC]: [],
|
||||||
[DatabaseType.MARIADB]: [],
|
[DatabaseType.MARIADB]: [],
|
||||||
[DatabaseType.CLICKHOUSE]: [],
|
[DatabaseType.CLICKHOUSE]: [],
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const createFieldsFromMetadata = ({
|
|||||||
nullable: col.nullable,
|
nullable: col.nullable,
|
||||||
...(col.character_maximum_length &&
|
...(col.character_maximum_length &&
|
||||||
col.character_maximum_length !== 'null'
|
col.character_maximum_length !== 'null'
|
||||||
? { character_maximum_length: col.character_maximum_length }
|
? { characterMaximumLength: col.character_maximum_length }
|
||||||
: {}),
|
: {}),
|
||||||
...(col.precision?.precision
|
...(col.precision?.precision
|
||||||
? { precision: col.precision.precision }
|
? { precision: col.precision.precision }
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
export const OPENAI_API_KEY: string = import.meta.env.VITE_OPENAI_API_KEY;
|
export const OPENAI_API_KEY: string = import.meta.env.VITE_OPENAI_API_KEY;
|
||||||
|
export const OPENAI_API_ENDPOINT: string = import.meta.env
|
||||||
|
.VITE_OPENAI_API_ENDPOINT;
|
||||||
|
export const LLM_MODEL_NAME: string = import.meta.env.VITE_LLM_MODEL_NAME;
|
||||||
export const IS_CHARTDB_IO: boolean =
|
export const IS_CHARTDB_IO: boolean =
|
||||||
import.meta.env.VITE_IS_CHARTDB_IO === 'true';
|
import.meta.env.VITE_IS_CHARTDB_IO === 'true';
|
||||||
export const APP_URL: string = import.meta.env.VITE_APP_URL;
|
export const APP_URL: string = import.meta.env.VITE_APP_URL;
|
||||||
export const HOST_URL: string = import.meta.env.VITE_HOST_URL ?? '';
|
export const HOST_URL: string = import.meta.env.VITE_HOST_URL ?? '';
|
||||||
|
export const HIDE_BUCKLE_DOT_DEV: boolean =
|
||||||
|
(window?.env?.HIDE_BUCKLE_DOT_DEV ??
|
||||||
|
import.meta.env.VITE_HIDE_BUCKLE_DOT_DEV) === 'true';
|
||||||
|
|||||||
@@ -45,10 +45,7 @@ import { Badge } from '@/components/badge/badge';
|
|||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import {
|
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
|
||||||
adjustTablePositions,
|
|
||||||
shouldShowTablesBySchemaFilter,
|
|
||||||
} from '@/lib/domain/db-table';
|
|
||||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -64,7 +61,7 @@ import {
|
|||||||
findTableOverlapping,
|
findTableOverlapping,
|
||||||
} from './canvas-utils';
|
} from './canvas-utils';
|
||||||
import type { Graph } from '@/lib/graph';
|
import type { Graph } from '@/lib/graph';
|
||||||
import { createGraph, removeVertex } from '@/lib/graph';
|
import { removeVertex } from '@/lib/graph';
|
||||||
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
|
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
|
||||||
import { cn, debounce, getOperatingSystem } from '@/lib/utils';
|
import { cn, debounce, getOperatingSystem } from '@/lib/utils';
|
||||||
import type { DependencyEdgeType } from './dependency-edge';
|
import type { DependencyEdgeType } from './dependency-edge';
|
||||||
@@ -76,6 +73,7 @@ import {
|
|||||||
} from './table-node/table-node-dependency-indicator';
|
} from './table-node/table-node-dependency-indicator';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import { useAlert } from '@/context/alert-context/alert-context';
|
import { useAlert } from '@/context/alert-context/alert-context';
|
||||||
|
import { useCanvas } from '@/hooks/use-canvas';
|
||||||
|
|
||||||
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
|
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
|
||||||
|
|
||||||
@@ -105,12 +103,10 @@ const tableToTableNode = (
|
|||||||
|
|
||||||
export interface CanvasProps {
|
export interface CanvasProps {
|
||||||
initialTables: DBTable[];
|
initialTables: DBTable[];
|
||||||
readonly?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||||
const { getEdge, getInternalNode, fitView, getEdges, getNode } =
|
const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
|
||||||
useReactFlow();
|
|
||||||
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
|
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
|
||||||
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
|
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
|
||||||
string[]
|
string[]
|
||||||
@@ -130,6 +126,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
filteredSchemas,
|
filteredSchemas,
|
||||||
events,
|
events,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
readonly,
|
||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
const { showSidePanel } = useLayout();
|
const { showSidePanel } = useLayout();
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
@@ -140,10 +137,10 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
|
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
|
||||||
const [highlightOverlappingTables, setHighlightOverlappingTables] =
|
const [highlightOverlappingTables, setHighlightOverlappingTables] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const { reorderTables, fitView, setOverlapGraph, overlapGraph } =
|
||||||
|
useCanvas();
|
||||||
|
|
||||||
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
|
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
|
||||||
const [overlapGraph, setOverlapGraph] =
|
|
||||||
useState<Graph<string>>(createGraph());
|
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState<TableNodeType>(
|
const [nodes, setNodes, onNodesChange] = useNodesState<TableNodeType>(
|
||||||
initialTables.map((table) => tableToTableNode(table, filteredSchemas))
|
initialTables.map((table) => tableToTableNode(table, filteredSchemas))
|
||||||
@@ -345,7 +342,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
}, 500)();
|
}, 500)();
|
||||||
prevFilteredSchemas.current = filteredSchemas;
|
prevFilteredSchemas.current = filteredSchemas;
|
||||||
}
|
}
|
||||||
}, [filteredSchemas, fitView, tables]);
|
}, [filteredSchemas, fitView, tables, setOverlapGraph]);
|
||||||
|
|
||||||
const onConnectHandler = useCallback(
|
const onConnectHandler = useCallback(
|
||||||
async (params: AddEdgeParams) => {
|
async (params: AddEdgeParams) => {
|
||||||
@@ -657,33 +654,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
const isLoadingDOM =
|
const isLoadingDOM =
|
||||||
tables.length > 0 ? !getInternalNode(tables[0].id) : false;
|
tables.length > 0 ? !getInternalNode(tables[0].id) : false;
|
||||||
|
|
||||||
const reorderTables = useCallback(() => {
|
|
||||||
const newTables = adjustTablePositions({
|
|
||||||
relationships,
|
|
||||||
tables: tables.filter((table) =>
|
|
||||||
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
|
||||||
),
|
|
||||||
mode: 'all', // Use 'all' mode for manual reordering
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedOverlapGraph = findOverlappingTables({
|
|
||||||
tables: newTables,
|
|
||||||
});
|
|
||||||
|
|
||||||
updateTablesState((currentTables) =>
|
|
||||||
currentTables.map((table) => {
|
|
||||||
const newTable = newTables.find((t) => t.id === table.id);
|
|
||||||
return {
|
|
||||||
id: table.id,
|
|
||||||
x: newTable?.x ?? table.x,
|
|
||||||
y: newTable?.y ?? table.y,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setOverlapGraph(updatedOverlapGraph);
|
|
||||||
}, [filteredSchemas, relationships, tables, updateTablesState]);
|
|
||||||
|
|
||||||
const showReorderConfirmation = useCallback(() => {
|
const showReorderConfirmation = useCallback(() => {
|
||||||
showAlert({
|
showAlert({
|
||||||
title: t('reorder_diagram_alert.title'),
|
title: t('reorder_diagram_alert.title'),
|
||||||
@@ -712,7 +682,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasContextMenu>
|
<CanvasContextMenu>
|
||||||
<div className="relative flex h-full">
|
<div className="relative flex h-full" id="canvas">
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
colorMode={effectiveTheme}
|
colorMode={effectiveTheme}
|
||||||
className="canvas-cursor-default nodes-animated"
|
className="canvas-cursor-default nodes-animated"
|
||||||
|
|||||||
86
src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
Normal file
86
src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { getTableDimensions } from '../canvas-utils';
|
||||||
|
import type { TableNodeType } from '../table-node/table-node';
|
||||||
|
import { useOnViewportChange, useReactFlow } from '@xyflow/react';
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce-v2';
|
||||||
|
|
||||||
|
export const useIsLostInCanvas = () => {
|
||||||
|
const { getNodes, getViewport } = useReactFlow();
|
||||||
|
const [noTablesVisible, setNoTablesVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Check if any tables are visible in the current viewport
|
||||||
|
const checkVisibleTables = useCallback(() => {
|
||||||
|
const nodes = getNodes();
|
||||||
|
const viewport = getViewport();
|
||||||
|
|
||||||
|
// If there are no nodes at all, don't highlight the button
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
setNoTablesVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count visible (not hidden) nodes
|
||||||
|
const visibleNodes = nodes.filter((node) => !node.hidden);
|
||||||
|
|
||||||
|
// If there are no visible nodes at all, don't highlight the button
|
||||||
|
if (visibleNodes.length === 0) {
|
||||||
|
setNoTablesVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate viewport boundaries
|
||||||
|
const viewportLeft = -viewport.x / viewport.zoom;
|
||||||
|
const viewportTop = -viewport.y / viewport.zoom;
|
||||||
|
|
||||||
|
const width =
|
||||||
|
document.getElementById('canvas')?.clientWidth || window.innerWidth;
|
||||||
|
const height =
|
||||||
|
document.getElementById('canvas')?.clientHeight ||
|
||||||
|
window.innerHeight;
|
||||||
|
|
||||||
|
const viewportRight = viewportLeft + width / viewport.zoom;
|
||||||
|
const viewportBottom = viewportTop + height / viewport.zoom;
|
||||||
|
|
||||||
|
// Check if any node is visible in the viewport
|
||||||
|
const anyNodeVisible = visibleNodes.some((node) => {
|
||||||
|
let nodeWidth = node.width || 0;
|
||||||
|
let nodeHeight = node.height || 0;
|
||||||
|
|
||||||
|
if (node.type === 'table' && node.data?.table) {
|
||||||
|
const tableNodeType = node as TableNodeType;
|
||||||
|
const dimensions = getTableDimensions(tableNodeType.data.table);
|
||||||
|
nodeWidth = dimensions.width;
|
||||||
|
nodeHeight = dimensions.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node boundaries
|
||||||
|
const nodeLeft = node.position.x;
|
||||||
|
const nodeTop = node.position.y;
|
||||||
|
const nodeRight = nodeLeft + nodeWidth;
|
||||||
|
const nodeBottom = nodeTop + nodeHeight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
nodeRight >= viewportLeft &&
|
||||||
|
nodeLeft <= viewportRight &&
|
||||||
|
nodeBottom >= viewportTop &&
|
||||||
|
nodeTop <= viewportBottom
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only set to true if there are tables but none are visible
|
||||||
|
setNoTablesVisible(!anyNodeVisible);
|
||||||
|
}, [getNodes, getViewport]);
|
||||||
|
|
||||||
|
// Create a debounced version of checkVisibleTables
|
||||||
|
const debouncedCheckVisibleTables = useDebounce(checkVisibleTables, 1000);
|
||||||
|
|
||||||
|
useOnViewportChange({
|
||||||
|
onEnd: () => {
|
||||||
|
debouncedCheckVisibleTables();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLostInCanvas: noTablesVisible,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import { useChartDB } from '@/hooks/use-chartdb';
|
|||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getCardinalityMarkerId } from './canvas-utils';
|
import { getCardinalityMarkerId } from './canvas-utils';
|
||||||
|
import { useDiff } from '@/context/diff-context/use-diff';
|
||||||
|
|
||||||
export type RelationshipEdgeType = Edge<
|
export type RelationshipEdgeType = Edge<
|
||||||
{
|
{
|
||||||
@@ -29,6 +30,7 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { getInternalNode, getEdge } = useReactFlow();
|
const { getInternalNode, getEdge } = useReactFlow();
|
||||||
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
|
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
|
||||||
|
const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
|
||||||
|
|
||||||
const { relationships } = useChartDB();
|
const { relationships } = useChartDB();
|
||||||
|
|
||||||
@@ -149,6 +151,25 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
|||||||
}),
|
}),
|
||||||
[relationship?.targetCardinality, selected, targetSide]
|
[relationship?.targetCardinality, selected, targetSide]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isDiffNewRelationship = useMemo(
|
||||||
|
() =>
|
||||||
|
relationship?.id
|
||||||
|
? checkIfNewRelationship({ relationshipId: relationship.id })
|
||||||
|
: false,
|
||||||
|
[checkIfNewRelationship, relationship?.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDiffRelationshipRemoved = useMemo(
|
||||||
|
() =>
|
||||||
|
relationship?.id
|
||||||
|
? checkIfRelationshipRemoved({
|
||||||
|
relationshipId: relationship.id,
|
||||||
|
})
|
||||||
|
: false,
|
||||||
|
[checkIfRelationshipRemoved, relationship?.id]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -160,6 +181,10 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
|||||||
className={cn([
|
className={cn([
|
||||||
'react-flow__edge-path',
|
'react-flow__edge-path',
|
||||||
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
|
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
|
||||||
|
{
|
||||||
|
'!stroke-green-500': isDiffNewRelationship,
|
||||||
|
'!stroke-red-500': isDiffRelationshipRemoved,
|
||||||
|
},
|
||||||
])}
|
])}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.detail === 2) {
|
if (e.detail === 2) {
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import { useChartDB } from '@/hooks/use-chartdb';
|
|||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
import { cloneTable } from '@/lib/clone';
|
import { cloneTable } from '@/lib/clone';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import { Copy, Pencil, Trash2 } from 'lucide-react';
|
import { Copy, Pencil, Trash2, Workflow } from 'lucide-react';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
|
||||||
export interface TableNodeContextMenuProps {
|
export interface TableNodeContextMenuProps {
|
||||||
table: DBTable;
|
table: DBTable;
|
||||||
@@ -24,6 +25,7 @@ export const TableNodeContextMenu: React.FC<
|
|||||||
const { openTableFromSidebar } = useLayout();
|
const { openTableFromSidebar } = useLayout();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
const { isMd: isDesktop } = useBreakpoint('md');
|
||||||
|
const { openCreateRelationshipDialog } = useDialog();
|
||||||
|
|
||||||
const duplicateTableHandler = useCallback(() => {
|
const duplicateTableHandler = useCallback(() => {
|
||||||
const clonedTable = cloneTable(table);
|
const clonedTable = cloneTable(table);
|
||||||
@@ -43,6 +45,12 @@ export const TableNodeContextMenu: React.FC<
|
|||||||
removeTable(table.id);
|
removeTable(table.id);
|
||||||
}, [removeTable, table.id]);
|
}, [removeTable, table.id]);
|
||||||
|
|
||||||
|
const addRelationshipHandler = useCallback(() => {
|
||||||
|
openCreateRelationshipDialog({
|
||||||
|
sourceTableId: table.id,
|
||||||
|
});
|
||||||
|
}, [openCreateRelationshipDialog, table.id]);
|
||||||
|
|
||||||
if (!isDesktop || readonly) {
|
if (!isDesktop || readonly) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
@@ -64,6 +72,13 @@ export const TableNodeContextMenu: React.FC<
|
|||||||
<span>{t('table_node_context_menu.duplicate_table')}</span>
|
<span>{t('table_node_context_menu.duplicate_table')}</span>
|
||||||
<Copy className="size-3.5" />
|
<Copy className="size-3.5" />
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={addRelationshipHandler}
|
||||||
|
className="flex justify-between gap-3"
|
||||||
|
>
|
||||||
|
<span>{t('table_node_context_menu.add_relationship')}</span>
|
||||||
|
<Workflow className="size-3.5" />
|
||||||
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={removeTableHandler}
|
onClick={removeTableHandler}
|
||||||
className="flex justify-between gap-3"
|
className="flex justify-between gap-3"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user