Compare commits
197 Commits
v1.3.1
...
jf/fix_sql
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f5533c071 | ||
|
|
5b9a88a8f3 | ||
|
|
2a8714a564 | ||
|
|
67f5ac303e | ||
|
|
578546a171 | ||
|
|
aa0b629a3e | ||
|
|
69beaa0a83 | ||
|
|
4fcc49d49a | ||
|
|
d15985e399 | ||
|
|
d429128e65 | ||
|
|
2fce8326b6 | ||
|
|
433c68a33d | ||
|
|
58acb65f12 | ||
|
|
7978955819 | ||
|
|
c6118e0cdb | ||
|
|
7d063b905f | ||
|
|
e0ff198c3f | ||
|
|
8b86e1c229 | ||
|
|
24be28a662 | ||
|
|
c6788b4917 | ||
|
|
4a52bf02e6 | ||
|
|
08b627cb8c | ||
|
|
73f542adad | ||
|
|
0d11b0c55a | ||
|
|
5b9d2bd1e3 | ||
|
|
cf1e141837 | ||
|
|
3894a22174 | ||
|
|
cad155e655 | ||
|
|
4477b1ca1f | ||
|
|
cd443466c7 | ||
|
|
18012ddab1 | ||
|
|
beb015194f | ||
|
|
c3904d9fdd | ||
|
|
aee5779983 | ||
|
|
765a1c4354 | ||
|
|
86840a8822 | ||
|
|
487fb2d5c1 | ||
|
|
54d5e96a6d | ||
|
|
481ad3c844 | ||
|
|
0ce85cf76b | ||
|
|
5849e4586c | ||
|
|
34c0a7163f | ||
|
|
89e3ceab00 | ||
|
|
5a5e64abef | ||
|
|
2368e0d263 | ||
|
|
547149da44 | ||
|
|
a1144bbf76 | ||
|
|
6b8d637b75 | ||
|
|
fd47eb7f4b | ||
|
|
7db86dcf8c | ||
|
|
e75323c16e | ||
|
|
97d01d7201 | ||
|
|
90b42a4bb7 | ||
|
|
fbf2fe919c | ||
|
|
d3ddf7c51e | ||
|
|
5759241573 | ||
|
|
3747abbc3b | ||
|
|
226e6cf1ce | ||
|
|
1778abb683 | ||
|
|
90a20dd1b0 | ||
|
|
21c9129e14 | ||
|
|
19d2d0bddd | ||
|
|
83c43332d4 | ||
|
|
3a1b8d1db1 | ||
|
|
46426e27b4 | ||
|
|
9402822fa3 | ||
|
|
651fe361fc | ||
|
|
aee1713aec | ||
|
|
ecfa14829b | ||
|
|
92e3ec785c | ||
|
|
8102f19f79 | ||
|
|
840a00ebcd | ||
|
|
181f96d250 | ||
|
|
ce2389f135 | ||
|
|
f15dc77f33 | ||
|
|
caa81c24a6 | ||
|
|
e3cb62788c | ||
|
|
fc46cbb893 | ||
|
|
d94a71e9e1 | ||
|
|
cf81253535 | ||
|
|
25c4b42538 | ||
|
|
f7a6e0cb5e | ||
|
|
85275e5dd6 | ||
|
|
4e5b467ce5 | ||
|
|
874aa5ab75 | ||
|
|
0940d72d5d | ||
|
|
0d1739d70f | ||
|
|
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 | ||
|
|
74c1730425 | ||
|
|
94bed7fcce | ||
|
|
8abf2a7bfc | ||
|
|
ee659eaa03 | ||
|
|
7c5db0848e | ||
|
|
4b43f720e9 | ||
|
|
766b5164b8 | ||
|
|
7868ca9f42 | ||
|
|
0411742864 | ||
|
|
9831ac5a10 | ||
|
|
91c6fb9249 | ||
|
|
c155013668 | ||
|
|
1b0f293c87 | ||
|
|
df2dc03aa0 | ||
|
|
205d431c89 | ||
|
|
0abe18cdf9 | ||
|
|
a151f56b5d | ||
|
|
2b6b733261 | ||
|
|
b56b04925c | ||
|
|
635fb53c9f | ||
|
|
d6659795bc | ||
|
|
348f80568e | ||
|
|
5f9c74a9ad | ||
|
|
5409288388 | ||
|
|
2309306ef5 | ||
|
|
3574cecc7c | ||
|
|
29b8edc051 | ||
|
|
5fc10a7e64 | ||
|
|
807cd22e0c | ||
|
|
03772f6b4f | ||
|
|
885eb719de | ||
|
|
94656ec7a5 | ||
|
|
a0e966b64f | ||
|
|
a8fe491c1b | ||
|
|
ddeef3b134 | ||
|
|
d45677e92d | ||
|
|
9c7d03c285 | ||
|
|
be1b109f23 | ||
|
|
05eaf85a3d |
@@ -1,29 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:css-modules/recommended',
|
||||
'plugin:tailwindcss/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
// 'plugin:jsx-a11y/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh', 'css-modules', 'tailwindcss', 'jsx-a11y'],
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: { version: 'detect' },
|
||||
},
|
||||
};
|
||||
33
.github/workflows/cla.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened,closed,synchronize]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write # this can be 'read' if the signatures are in remote repository
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
|
||||
jobs:
|
||||
CLAAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: contributor-assistant/github-action@v2.6.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.CHARTDB_CLA_SIGNATURES_PAT }}
|
||||
with:
|
||||
remote-organization-name: 'chartdb'
|
||||
remote-repository-name: 'cla-signatures'
|
||||
path-to-signatures: 'signatures/version1/cla.json'
|
||||
path-to-document: 'https://github.com/chartdb/chartdb/blob/main/CLA.md'
|
||||
# branch should not be protected
|
||||
branch: 'main'
|
||||
allowlist:
|
||||
11
.github/workflows/publish.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -42,6 +42,12 @@ jobs:
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
@@ -50,10 +56,11 @@ jobs:
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
- name: Build and push multi-arch Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
97
.husky/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Smart Pre-commit Hooks
|
||||
|
||||
This directory contains intelligent pre-commit hooks that run relevant tests based on the files being committed.
|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Test Detection**: Automatically detects which tests to run based on changed files
|
||||
- **Configurable Mappings**: Easy to configure via `test-mapping.json` (optional)
|
||||
- **Performance Optimized**: Only runs tests for affected code
|
||||
- **Skip Option**: Temporarily skip tests when needed
|
||||
- **Progressive Enhancement**: Works without dependencies, enhanced with `jq` if available
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Linting**: Always runs linting first
|
||||
2. **File Analysis**: Examines staged files to determine which are SQL import related
|
||||
3. **Test Selection**: Maps changed files to relevant test suites
|
||||
4. **Test Execution**: Runs only the necessary tests
|
||||
|
||||
## Configuration
|
||||
|
||||
The test runner works in two modes:
|
||||
|
||||
### Basic Mode (No Dependencies)
|
||||
- Uses built-in patterns for common SQL import files
|
||||
- Works out of the box without any additional tools
|
||||
|
||||
### Enhanced Mode (With `jq`)
|
||||
- Reads configuration from `test-mapping.json`
|
||||
- Allows custom patterns and mappings
|
||||
- More flexible and maintainable
|
||||
|
||||
### Automatic Behaviors
|
||||
- **Documentation Changes**: Tests are automatically skipped for .md, .txt, and .rst files
|
||||
- **Verbose Output**: Always shows matched files and test paths for better visibility
|
||||
|
||||
## File Mappings
|
||||
|
||||
Built-in mappings:
|
||||
- PostgreSQL import files → PostgreSQL tests
|
||||
- MySQL import files → MySQL tests
|
||||
- SQLite import files → SQLite tests
|
||||
- SQL Server import files → SQL Server tests
|
||||
- Common SQL files → All dialect tests
|
||||
- SQL validator → PostgreSQL tests
|
||||
|
||||
## Usage
|
||||
|
||||
### Normal Operation
|
||||
Just commit as usual. The hooks will automatically run relevant tests.
|
||||
|
||||
### Skip Tests Temporarily
|
||||
```bash
|
||||
# Create skip file
|
||||
touch .husky/.skip-tests
|
||||
|
||||
# Commit without tests
|
||||
git commit -m "WIP: debugging"
|
||||
|
||||
# Remove skip file to re-enable
|
||||
rm .husky/.skip-tests
|
||||
```
|
||||
|
||||
### Customize Mappings
|
||||
1. Install `jq`: `brew install jq` (macOS) or `apt-get install jq` (Linux)
|
||||
2. Edit `test-mapping.json` to add new patterns or modify existing ones
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Required**: None (works with bash only)
|
||||
- **Optional**: `jq` for JSON configuration support
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: PostgreSQL Parser Change
|
||||
```bash
|
||||
# Changed: src/lib/data/sql-import/dialect-importers/postgresql/postgresql-improved.ts
|
||||
# Runs: src/lib/data/sql-import/dialect-importers/postgresql/__tests__
|
||||
```
|
||||
|
||||
### Example 2: Common SQL Import Change
|
||||
```bash
|
||||
# Changed: src/lib/data/sql-import/common.ts
|
||||
# Runs: All dialect tests (PostgreSQL, MySQL, SQLite, SQL Server)
|
||||
```
|
||||
|
||||
### Example 3: Test File Change
|
||||
```bash
|
||||
# Changed: src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-types.test.ts
|
||||
# Runs: That specific test file
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Tests not running**: Check if `.husky/.skip-tests` exists
|
||||
2. **Wrong tests running**: Check `test-mapping.json` patterns
|
||||
3. **All tests running**: You may have exceeded the change threshold
|
||||
@@ -1,2 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Run linting first
|
||||
npm run lint || { echo "lint failed, please run \"npm run lint:fix\" to fix the errors." ; exit 1; }
|
||||
|
||||
# Check if tests should be skipped
|
||||
if [ -f .husky/.skip-tests ]; then
|
||||
echo "⚠️ Tests skipped (remove .husky/.skip-tests to enable)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run smart test runner for SQL import related changes
|
||||
.husky/smart-test-runner.sh || exit 1
|
||||
|
||||
214
.husky/smart-test-runner.sh
Executable file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Get the directory of this script
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
CONFIG_FILE="$SCRIPT_DIR/test-mapping.json"
|
||||
|
||||
# Get list of staged files
|
||||
STAGED_FILES=$(git diff --cached --name-only)
|
||||
|
||||
# Check if only documentation files are staged
|
||||
DOC_ONLY=true
|
||||
NON_DOC_COUNT=0
|
||||
while IFS= read -r file; do
|
||||
[ -z "$file" ] && continue
|
||||
if [[ ! "$file" =~ \.(md|txt|rst)$ ]]; then
|
||||
DOC_ONLY=false
|
||||
((NON_DOC_COUNT++))
|
||||
fi
|
||||
done <<< "$STAGED_FILES"
|
||||
|
||||
# Skip tests if only docs are changed
|
||||
if [ "$DOC_ONLY" = "true" ]; then
|
||||
echo -e "${YELLOW}ℹ️ Only documentation files changed, skipping tests.${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Initialize test tracking
|
||||
TESTS_TO_RUN=""
|
||||
MATCHED_FILES=()
|
||||
|
||||
# Function to add test path
|
||||
add_test() {
|
||||
local test_path=$1
|
||||
if [ -d "$test_path" ] || [ -f "$test_path" ]; then
|
||||
# Add to list if not already present
|
||||
if [[ ! "$TESTS_TO_RUN" =~ "$test_path" ]]; then
|
||||
if [ -z "$TESTS_TO_RUN" ]; then
|
||||
TESTS_TO_RUN="$test_path"
|
||||
else
|
||||
TESTS_TO_RUN="$TESTS_TO_RUN $test_path"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check if file matches pattern (simple glob matching)
|
||||
matches_pattern() {
|
||||
local file=$1
|
||||
local pattern=$2
|
||||
|
||||
# Use bash pattern matching
|
||||
case "$file" in
|
||||
$pattern) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Always verbose by default
|
||||
VERBOSE=true
|
||||
|
||||
# Process files based on available tools
|
||||
if command -v jq &> /dev/null && [ -f "$CONFIG_FILE" ]; then
|
||||
echo -e "${YELLOW}Using configuration from test-mapping.json${NC}"
|
||||
|
||||
# Process each staged file
|
||||
while IFS= read -r file; do
|
||||
[ -z "$file" ] && continue
|
||||
|
||||
# Check against each mapping rule
|
||||
jq -c '.mappings[]' "$CONFIG_FILE" 2>/dev/null | while read -r mapping; do
|
||||
name=$(echo "$mapping" | jq -r '.name')
|
||||
|
||||
# Check patterns
|
||||
echo "$mapping" | jq -r '.patterns[]' | while read -r pattern; do
|
||||
if matches_pattern "$file" "$pattern"; then
|
||||
# Check exclusions
|
||||
excluded=false
|
||||
echo "$mapping" | jq -r '.excludePatterns[]?' 2>/dev/null | while read -r exclude; do
|
||||
if matches_pattern "$file" "$exclude"; then
|
||||
excluded=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$excluded" = "false" ]; then
|
||||
[ "$VERBOSE" = "true" ] && echo -e "${GREEN}✓ Matched rule '$name' for file: $file${NC}"
|
||||
MATCHED_FILES+=("$file")
|
||||
|
||||
# Add tests for this mapping
|
||||
echo "$mapping" | jq -r '.tests[]' | while read -r test_path; do
|
||||
[ -n "$test_path" ] && echo "$test_path" >> /tmp/test_paths_$$
|
||||
done
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
done <<< "$STAGED_FILES"
|
||||
|
||||
# Read test paths from temp file
|
||||
if [ -f /tmp/test_paths_$$ ]; then
|
||||
while read -r test_path; do
|
||||
add_test "$test_path"
|
||||
done < /tmp/test_paths_$$
|
||||
rm -f /tmp/test_paths_$$
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}Using built-in patterns (install jq for config file support)${NC}"
|
||||
|
||||
# Fallback to hardcoded patterns
|
||||
while IFS= read -r file; do
|
||||
[ -z "$file" ] && continue
|
||||
|
||||
case "$file" in
|
||||
# PostgreSQL import files
|
||||
src/lib/data/sql-import/dialect-importers/postgresql/*.ts)
|
||||
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]]; then
|
||||
[ "$VERBOSE" = "true" ] && echo "📝 Changed PostgreSQL import file: $file"
|
||||
MATCHED_FILES+=("$file")
|
||||
add_test "src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
|
||||
fi
|
||||
;;
|
||||
|
||||
# MySQL import files
|
||||
src/lib/data/sql-import/dialect-importers/mysql/*.ts)
|
||||
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]]; then
|
||||
[ "$VERBOSE" = "true" ] && echo "📝 Changed MySQL import file: $file"
|
||||
MATCHED_FILES+=("$file")
|
||||
add_test "src/lib/data/sql-import/dialect-importers/mysql/__tests__"
|
||||
fi
|
||||
;;
|
||||
|
||||
# SQLite import files
|
||||
src/lib/data/sql-import/dialect-importers/sqlite/*.ts)
|
||||
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]]; then
|
||||
[ "$VERBOSE" = "true" ] && echo "📝 Changed SQLite import file: $file"
|
||||
MATCHED_FILES+=("$file")
|
||||
add_test "src/lib/data/sql-import/dialect-importers/sqlite/__tests__"
|
||||
fi
|
||||
;;
|
||||
|
||||
# SQL Server import files
|
||||
src/lib/data/sql-import/dialect-importers/sql-server/*.ts)
|
||||
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]]; then
|
||||
[ "$VERBOSE" = "true" ] && echo "📝 Changed SQL Server import file: $file"
|
||||
MATCHED_FILES+=("$file")
|
||||
add_test "src/lib/data/sql-import/dialect-importers/sql-server/__tests__"
|
||||
fi
|
||||
;;
|
||||
|
||||
# Common SQL import files
|
||||
src/lib/data/sql-import/*.ts)
|
||||
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]] && [[ ! "$file" =~ /dialect-importers/ ]]; then
|
||||
[ "$VERBOSE" = "true" ] && echo "📝 Changed common SQL import file: $file"
|
||||
MATCHED_FILES+=("$file")
|
||||
# Run all dialect tests if common files change
|
||||
add_test "src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
|
||||
add_test "src/lib/data/sql-import/dialect-importers/mysql/__tests__"
|
||||
add_test "src/lib/data/sql-import/dialect-importers/sqlite/__tests__"
|
||||
add_test "src/lib/data/sql-import/dialect-importers/sql-server/__tests__"
|
||||
fi
|
||||
;;
|
||||
|
||||
# SQL validator
|
||||
src/lib/data/sql-import/sql-validator.ts)
|
||||
[ "$VERBOSE" = "true" ] && echo "📝 Changed SQL validator"
|
||||
MATCHED_FILES+=("$file")
|
||||
add_test "src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
|
||||
;;
|
||||
|
||||
# Test files themselves
|
||||
src/lib/data/sql-import/**/*.test.ts|src/lib/data/sql-import/**/*.spec.ts)
|
||||
[ "$VERBOSE" = "true" ] && echo "📝 Changed test file: $file"
|
||||
MATCHED_FILES+=("$file")
|
||||
add_test "$file"
|
||||
;;
|
||||
esac
|
||||
done <<< "$STAGED_FILES"
|
||||
fi
|
||||
|
||||
# Run tests if any were found
|
||||
if [ -n "$TESTS_TO_RUN" ]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${YELLOW}🧪 Running SQL import tests...${NC}"
|
||||
[ "$VERBOSE" = "true" ] && echo -e "Matched files: ${#MATCHED_FILES[@]}"
|
||||
[ "$VERBOSE" = "true" ] && echo -e "Test paths: $TESTS_TO_RUN"
|
||||
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
|
||||
# Run the tests
|
||||
npm test -- $TESTS_TO_RUN --run
|
||||
TEST_RESULT=$?
|
||||
|
||||
if [ $TEST_RESULT -ne 0 ]; then
|
||||
echo ""
|
||||
echo -e "${RED}❌ SQL import tests failed! Please fix the tests before committing.${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ SQL import tests passed!${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}ℹ️ No SQL import related changes detected, skipping SQL import tests.${NC}"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
95
.husky/test-mapping.json
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"mappings": [
|
||||
{
|
||||
"name": "PostgreSQL Import",
|
||||
"patterns": [
|
||||
"src/lib/data/sql-import/dialect-importers/postgresql/*.ts"
|
||||
],
|
||||
"excludePatterns": [
|
||||
"*.test.ts",
|
||||
"*.spec.ts"
|
||||
],
|
||||
"tests": [
|
||||
"src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MySQL Import",
|
||||
"patterns": [
|
||||
"src/lib/data/sql-import/dialect-importers/mysql/*.ts"
|
||||
],
|
||||
"excludePatterns": [
|
||||
"*.test.ts",
|
||||
"*.spec.ts"
|
||||
],
|
||||
"tests": [
|
||||
"src/lib/data/sql-import/dialect-importers/mysql/__tests__"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SQLite Import",
|
||||
"patterns": [
|
||||
"src/lib/data/sql-import/dialect-importers/sqlite/*.ts"
|
||||
],
|
||||
"excludePatterns": [
|
||||
"*.test.ts",
|
||||
"*.spec.ts"
|
||||
],
|
||||
"tests": [
|
||||
"src/lib/data/sql-import/dialect-importers/sqlite/__tests__"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SQL Server Import",
|
||||
"patterns": [
|
||||
"src/lib/data/sql-import/dialect-importers/sql-server/*.ts"
|
||||
],
|
||||
"excludePatterns": [
|
||||
"*.test.ts",
|
||||
"*.spec.ts"
|
||||
],
|
||||
"tests": [
|
||||
"src/lib/data/sql-import/dialect-importers/sql-server/__tests__"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Common SQL Import",
|
||||
"patterns": [
|
||||
"src/lib/data/sql-import/*.ts",
|
||||
"src/lib/data/sql-import/common/*.ts"
|
||||
],
|
||||
"excludePatterns": [
|
||||
"*.test.ts",
|
||||
"*.spec.ts",
|
||||
"*/dialect-importers/*"
|
||||
],
|
||||
"tests": [
|
||||
"src/lib/data/sql-import/dialect-importers/postgresql/__tests__",
|
||||
"src/lib/data/sql-import/dialect-importers/mysql/__tests__",
|
||||
"src/lib/data/sql-import/dialect-importers/sqlite/__tests__",
|
||||
"src/lib/data/sql-import/dialect-importers/sql-server/__tests__"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SQL Validator",
|
||||
"patterns": [
|
||||
"src/lib/data/sql-import/sql-validator.ts"
|
||||
],
|
||||
"tests": [
|
||||
"src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Import Dialog",
|
||||
"patterns": [
|
||||
"src/dialogs/common/import-database/*.tsx",
|
||||
"src/dialogs/common/import-database/*.ts"
|
||||
],
|
||||
"excludePatterns": [
|
||||
"*.test.tsx",
|
||||
"*.spec.tsx"
|
||||
],
|
||||
"tests": []
|
||||
}
|
||||
]
|
||||
}
|
||||
275
CHANGELOG.md
@@ -1,5 +1,280 @@
|
||||
# Changelog
|
||||
|
||||
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add DISABLE_ANALYTICS flag to opt-out of Fathom analytics ([#750](https://github.com/chartdb/chartdb/issues/750)) ([aa0b629](https://github.com/chartdb/chartdb/commit/aa0b629a3eaf8e8b60473ea3f28f769270c7714c))
|
||||
|
||||
## [1.13.1](https://github.com/chartdb/chartdb/compare/v1.13.0...v1.13.1) (2025-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **custom_types:** fix display custom types in select box ([#737](https://github.com/chartdb/chartdb/issues/737)) ([24be28a](https://github.com/chartdb/chartdb/commit/24be28a662c48fc5bc62e76446b9669d83d7d3e0))
|
||||
* **dbml-editor:** for some cases that the dbml had issues ([#739](https://github.com/chartdb/chartdb/issues/739)) ([e0ff198](https://github.com/chartdb/chartdb/commit/e0ff198c3fd416498dac5680bb323ec88c54b65c))
|
||||
* **dbml:** Filter duplicate tables at diagram level before export dbml ([#746](https://github.com/chartdb/chartdb/issues/746)) ([d429128](https://github.com/chartdb/chartdb/commit/d429128e65aa28c500eac2487356e4869506e948))
|
||||
* **export-sql:** conditionally show generic option and reorder by diagram type ([#708](https://github.com/chartdb/chartdb/issues/708)) ([c6118e0](https://github.com/chartdb/chartdb/commit/c6118e0cdb0e5caaf73447d33db2fde1a98efe60))
|
||||
* general performance improvements on canvas ([#751](https://github.com/chartdb/chartdb/issues/751)) ([4fcc49d](https://github.com/chartdb/chartdb/commit/4fcc49d49a76a4b886ffd6cf0b40cf2fc49952ec))
|
||||
* **import-database:** for custom types query to import supabase & timescale ([#745](https://github.com/chartdb/chartdb/issues/745)) ([2fce832](https://github.com/chartdb/chartdb/commit/2fce8326b67b751d38dd34f409fea574449d0298))
|
||||
* **import-db:** fix mariadb import ([#740](https://github.com/chartdb/chartdb/issues/740)) ([7d063b9](https://github.com/chartdb/chartdb/commit/7d063b905f19f51501468bd0bd794a25cf65e1be))
|
||||
* **performance:** improve storage provider performance ([#734](https://github.com/chartdb/chartdb/issues/734)) ([c6788b4](https://github.com/chartdb/chartdb/commit/c6788b49173d9cce23571daeb460285cb7cffb11))
|
||||
* resolve unresponsive cursor and input glitches when editing field comments ([#749](https://github.com/chartdb/chartdb/issues/749)) ([d15985e](https://github.com/chartdb/chartdb/commit/d15985e3999a0cd54213b2fb08c55d48a1b8b3b2))
|
||||
* **table name:** updates table name value when its updated from canvas/sidebar ([#716](https://github.com/chartdb/chartdb/issues/716)) ([8b86e1c](https://github.com/chartdb/chartdb/commit/8b86e1c22992aaadcce7ad5fc1d267c5a57a99f0))
|
||||
|
||||
## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **custom-types:** add enums and composite types for Postgres ([#714](https://github.com/chartdb/chartdb/issues/714)) ([c3904d9](https://github.com/chartdb/chartdb/commit/c3904d9fdd63ef5b76a44e73582d592f2c418687))
|
||||
* **export-sql:** add custom types to export sql script ([#720](https://github.com/chartdb/chartdb/issues/720)) ([cad155e](https://github.com/chartdb/chartdb/commit/cad155e6550f171b8faecbfdff27032798ecea43))
|
||||
* **oracle:** support oracle in ChartDB ([#709](https://github.com/chartdb/chartdb/issues/709)) ([765a1c4](https://github.com/chartdb/chartdb/commit/765a1c43547a29bd3428c942c7afb56f63aaf046))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **canvas:** prevent canvas blink and lag on field edit ([#723](https://github.com/chartdb/chartdb/issues/723)) ([cd44346](https://github.com/chartdb/chartdb/commit/cd443466c7952f1cdc3739645c12130b9231e3a1))
|
||||
* **canvas:** prevent canvas blink and lag on primary field edit ([#725](https://github.com/chartdb/chartdb/issues/725)) ([4477b1c](https://github.com/chartdb/chartdb/commit/4477b1ca1fe6b282b604739a23e31181acd4d7bc))
|
||||
* **custom_types:** fix custom types on storage provider ([#721](https://github.com/chartdb/chartdb/issues/721)) ([beb0151](https://github.com/chartdb/chartdb/commit/beb015194f917c0ba644458410162d2b7599918c))
|
||||
* **custom_types:** fix custom types on storage provider ([#722](https://github.com/chartdb/chartdb/issues/722)) ([18012dd](https://github.com/chartdb/chartdb/commit/18012ddab1718bcce3432aea626adf6fc9be25d9))
|
||||
* **custom-types:** fetch directly via the smart-query the custom types ([#729](https://github.com/chartdb/chartdb/issues/729)) ([cf1e141](https://github.com/chartdb/chartdb/commit/cf1e141837eda77d717ad87489ce9946b688e226))
|
||||
* **dbml-editor:** export comments with schema if existsed ([#728](https://github.com/chartdb/chartdb/issues/728)) ([73f542a](https://github.com/chartdb/chartdb/commit/73f542adad2d66a1e84fc656a0c34d9b1f39f33c))
|
||||
* **dbml-editor:** fix export dbml - to show enums ([#724](https://github.com/chartdb/chartdb/issues/724)) ([3894a22](https://github.com/chartdb/chartdb/commit/3894a221745d32c13160bedcb1bcf53d89897698))
|
||||
* **import-database:** remove the default fetch from import database ([#718](https://github.com/chartdb/chartdb/issues/718)) ([0d11b0c](https://github.com/chartdb/chartdb/commit/0d11b0c55a94a12a764785cfdcf2ba10437241d6))
|
||||
* **menu:** add oracle to import menu ([#713](https://github.com/chartdb/chartdb/issues/713)) ([aee5779](https://github.com/chartdb/chartdb/commit/aee577998342eb4a2b05b3e03181992a435712d8))
|
||||
* **relationship:** fix creating of relationships ([#732](https://github.com/chartdb/chartdb/issues/732)) ([08b627c](https://github.com/chartdb/chartdb/commit/08b627cb8ca8fdf08d8ed2ff7e89104887deffb7))
|
||||
|
||||
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **areas:** implement area to enable logical diagram arrangement ([#661](https://github.com/chartdb/chartdb/issues/661)) ([92e3ec7](https://github.com/chartdb/chartdb/commit/92e3ec785c91f7f19881c6d9d0692257af4651bc))
|
||||
* **examples:** update examples to have areas ([#677](https://github.com/chartdb/chartdb/issues/677)) ([21c9129](https://github.com/chartdb/chartdb/commit/21c9129e14670c744950cd43a5cbdd4b7d47c639))
|
||||
* **image-export:** add transparent and pattern export image toggles ([#671](https://github.com/chartdb/chartdb/issues/671)) ([6b8d637](https://github.com/chartdb/chartdb/commit/6b8d637b757b94630ecd7521b4a2c99634afae69))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add sorting based on how common the datatype on side-panel ([#651](https://github.com/chartdb/chartdb/issues/651)) ([3a1b8d1](https://github.com/chartdb/chartdb/commit/3a1b8d1db13d8dd7cb6cbe5ef8c5a60faccfeae5))
|
||||
* **canvas:** disable edit area name on read only ([#666](https://github.com/chartdb/chartdb/issues/666)) ([9402822](https://github.com/chartdb/chartdb/commit/9402822fa31f8cd94fe7971277839ee5425e29bf))
|
||||
* **canvas:** read only mode ([#665](https://github.com/chartdb/chartdb/issues/665)) ([651fe36](https://github.com/chartdb/chartdb/commit/651fe361fce61fe0577d2593f268131e9ca359d0))
|
||||
* **clone:** add areas to clone diagram ([#664](https://github.com/chartdb/chartdb/issues/664)) ([aee1713](https://github.com/chartdb/chartdb/commit/aee1713aecdd5e54228a16cbc3c4fc184661c56b))
|
||||
* **dbml-editor:** add inline refs mode + fix issues with DBML syntax ([#687](https://github.com/chartdb/chartdb/issues/687)) ([fbf2fe9](https://github.com/chartdb/chartdb/commit/fbf2fe919c2168c715f8231c0246753b19635f14))
|
||||
* **dbml-editor:** remove invalid fields before showing DBML + warning ([#683](https://github.com/chartdb/chartdb/issues/683)) ([5759241](https://github.com/chartdb/chartdb/commit/5759241573db204183c92599588d59f4aadaeafb))
|
||||
* **ddl-import:** fix datatypes when importing via ddl ([#696](https://github.com/chartdb/chartdb/issues/696)) ([a1144bb](https://github.com/chartdb/chartdb/commit/a1144bbf761a0daedd546b5d9b92300be59e0157))
|
||||
* **ddl:** inline fks ddl script ([#701](https://github.com/chartdb/chartdb/issues/701)) ([5849e45](https://github.com/chartdb/chartdb/commit/5849e4586c7c2a7cd86bd064df8916b130fc6234))
|
||||
* **dependencies:** hide icon when diagram has no dependencies ([#684](https://github.com/chartdb/chartdb/issues/684)) ([547149d](https://github.com/chartdb/chartdb/commit/547149da44db6d3d1e36d619d475fe52ff83a472))
|
||||
* **examples:** add loader ([#678](https://github.com/chartdb/chartdb/issues/678)) ([90a20dd](https://github.com/chartdb/chartdb/commit/90a20dd1b0277c4aee848fae5ed7a8347c5ba77d))
|
||||
* **examples:** fix clone examples ([#679](https://github.com/chartdb/chartdb/issues/679)) ([1778abb](https://github.com/chartdb/chartdb/commit/1778abb683d575af244edcd9a11f8d03f903f719))
|
||||
* **expanded-table:** persist expanded state across renders ([#707](https://github.com/chartdb/chartdb/issues/707)) ([54d5e96](https://github.com/chartdb/chartdb/commit/54d5e96a6db1e3abd52229a89ac503ff31885386))
|
||||
* **export image:** Fix usage of advanced options accordion ([#703](https://github.com/chartdb/chartdb/issues/703)) ([0ce85cf](https://github.com/chartdb/chartdb/commit/0ce85cf76b733f441f661608278c0db3122c5074))
|
||||
* **import-database:** auto detect when user try to import ddl script ([#698](https://github.com/chartdb/chartdb/issues/698)) ([5a5e64a](https://github.com/chartdb/chartdb/commit/5a5e64abef510cff28b3d8972520d0b9df29b024))
|
||||
* **import-database:** remove view_definition when importing via query ([#702](https://github.com/chartdb/chartdb/issues/702)) ([481ad3c](https://github.com/chartdb/chartdb/commit/481ad3c8449f469bf2b4418e4cdcc5b5608dfd36))
|
||||
* **import-json:** for broken json imports ([#697](https://github.com/chartdb/chartdb/issues/697)) ([2368e0d](https://github.com/chartdb/chartdb/commit/2368e0d2639021c4a11a8e5131d6af44fb6a47db))
|
||||
* **import-json:** simplify import script for fixing invalid JSON ([#681](https://github.com/chartdb/chartdb/issues/681)) ([226e6cf](https://github.com/chartdb/chartdb/commit/226e6cf1ce4d2edcfbee6a4de7ab0bc0cfeb17fe))
|
||||
* **import:** dbml and query - senetize before import ([#699](https://github.com/chartdb/chartdb/issues/699)) ([34c0a71](https://github.com/chartdb/chartdb/commit/34c0a7163f47bde7ddfaa8f044341e3c971b7e03))
|
||||
* **navbar:** open diagram directly from diagram icon ([#694](https://github.com/chartdb/chartdb/issues/694)) ([7db86dc](https://github.com/chartdb/chartdb/commit/7db86dcf8c97d34b056e4b5b85a0dda0438322ea))
|
||||
* **performance:** Only render visible ([#672](https://github.com/chartdb/chartdb/issues/672)) ([83c4333](https://github.com/chartdb/chartdb/commit/83c43332d497e9fc148a18b9cb4d9ecc85e44183))
|
||||
* **performance:** update field only when changed ([#685](https://github.com/chartdb/chartdb/issues/685)) ([d3ddf7c](https://github.com/chartdb/chartdb/commit/d3ddf7c51eaa4b9cddb961defd52d423f39f281d))
|
||||
* **postgres:** fix import of postgres fks ([#700](https://github.com/chartdb/chartdb/issues/700)) ([89e3cea](https://github.com/chartdb/chartdb/commit/89e3ceab00defaabc079e165fc90e92ca00722cf))
|
||||
* **schema:** add areas to diagram schema ([#663](https://github.com/chartdb/chartdb/issues/663)) ([ecfa148](https://github.com/chartdb/chartdb/commit/ecfa14829bcb1b813c7b154b4bd59f24e3032d8f))
|
||||
* **sql-script:** change ddl to be sql-script ([#710](https://github.com/chartdb/chartdb/issues/710)) ([487fb2d](https://github.com/chartdb/chartdb/commit/487fb2d5c17b70ac54aa17af9a2ac9aded6b40ba))
|
||||
* **table:** enhance field focus behavior to include table hover state ([#676](https://github.com/chartdb/chartdb/issues/676)) ([19d2d0b](https://github.com/chartdb/chartdb/commit/19d2d0bddd3a464995b79e97e6caf6e652836081))
|
||||
* **translations:** Add some translations for ru-RU language ([#690](https://github.com/chartdb/chartdb/issues/690)) ([97d01d7](https://github.com/chartdb/chartdb/commit/97d01d72014e473c42348c9ebcbe7a0b973d31aa))
|
||||
|
||||
## [1.11.0](https://github.com/chartdb/chartdb/compare/v1.10.0...v1.11.0) (2025-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add sidebar footer help buttons ([#650](https://github.com/chartdb/chartdb/issues/650)) ([fc46cbb](https://github.com/chartdb/chartdb/commit/fc46cbb8933761c7bac3604664f7de812f6f5b6b))
|
||||
* **import-sql:** import postgresql via SQL (DDL script) ([#639](https://github.com/chartdb/chartdb/issues/639)) ([f7a6e0c](https://github.com/chartdb/chartdb/commit/f7a6e0cb5e4921dd9540739f9da269858e7ca7be))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **import:** display query result formatted ([#644](https://github.com/chartdb/chartdb/issues/644)) ([caa81c2](https://github.com/chartdb/chartdb/commit/caa81c24a6535bc87129c38622aac5a62a6d479d))
|
||||
* **import:** strict parse of database metadata ([#635](https://github.com/chartdb/chartdb/issues/635)) ([0940d72](https://github.com/chartdb/chartdb/commit/0940d72d5d3726650213257639f24ba47e729854))
|
||||
* **mobile:** fix create diagram modal on mobile ([#646](https://github.com/chartdb/chartdb/issues/646)) ([25c4b42](https://github.com/chartdb/chartdb/commit/25c4b4253849575d7a781ed197281e2a35e7184a))
|
||||
* **mysql-ddl:** update the script to import - for create fks ([#642](https://github.com/chartdb/chartdb/issues/642)) ([cf81253](https://github.com/chartdb/chartdb/commit/cf81253535ca5a3b8a65add78287c1bdb283a1c7))
|
||||
* **performance:** Import deps dynamically ([#652](https://github.com/chartdb/chartdb/issues/652)) ([e3cb627](https://github.com/chartdb/chartdb/commit/e3cb62788c13f149e35e1a5020191bd43d14b52f))
|
||||
* remove unused links from help menu ([#623](https://github.com/chartdb/chartdb/issues/623)) ([85275e5](https://github.com/chartdb/chartdb/commit/85275e5dd6e7845f06f682eeceda7932fc87e875))
|
||||
* **sidebar:** turn sidebar to responsive for mobile ([#658](https://github.com/chartdb/chartdb/issues/658)) ([ce2389f](https://github.com/chartdb/chartdb/commit/ce2389f135d399d82c9848335d31174bac8a3791))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **view-menu:** add toggle for mini map visibility ([#496](https://github.com/chartdb/chartdb/issues/496)) ([#505](https://github.com/chartdb/chartdb/issues/505)) ([8abf2a7](https://github.com/chartdb/chartdb/commit/8abf2a7bfcc36d39e60ac133b0e5e569de1bbc72))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add loadDiagramFromData logic to chartdb provider ([#513](https://github.com/chartdb/chartdb/issues/513)) ([ee659ea](https://github.com/chartdb/chartdb/commit/ee659eaa038a94ee13801801e84152df4d79683d))
|
||||
* **dependency:** upgrade react query to v7 - clean console warnings ([#504](https://github.com/chartdb/chartdb/issues/504)) ([7c5db08](https://github.com/chartdb/chartdb/commit/7c5db0848e49dfdb7e7120f77003d1e37f8d71b0))
|
||||
* **i18n:** translation/Arabic ([#509](https://github.com/chartdb/chartdb/issues/509)) ([4b43f72](https://github.com/chartdb/chartdb/commit/4b43f720e90e49d5461e68d188e3865000f52497))
|
||||
|
||||
## [1.5.1](https://github.com/chartdb/chartdb/compare/v1.5.0...v1.5.1) (2024-12-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **export:** fix SQL server field.nullable type to boolean ([#486](https://github.com/chartdb/chartdb/issues/486)) ([a151f56](https://github.com/chartdb/chartdb/commit/a151f56b5d950e0b5cc54363684ada95889024b3))
|
||||
* **readme:** Update README.md - add CockroachDB ([#482](https://github.com/chartdb/chartdb/issues/482)) ([2b6b733](https://github.com/chartdb/chartdb/commit/2b6b73326155f18d6d56779c0657a3506e2d2cde))
|
||||
|
||||
## [1.5.0](https://github.com/chartdb/chartdb/compare/v1.4.0...v1.5.0) (2024-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **CockroachDB:** Add CockroachDB support ([#472](https://github.com/chartdb/chartdb/issues/472)) ([5409288](https://github.com/chartdb/chartdb/commit/54092883883b135f6ace51d86754b1df76603d30))
|
||||
* **i18n:** translate share and dialog sections in Indonesian locale files ([#468](https://github.com/chartdb/chartdb/issues/468)) ([3574cec](https://github.com/chartdb/chartdb/commit/3574cecc7c73dcab404b82115d20e1ad0cd26b37))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** fix update diagram id ([#477](https://github.com/chartdb/chartdb/issues/477)) ([348f805](https://github.com/chartdb/chartdb/commit/348f80568e0f686ee478147fdc43a5d43b5c1ebb))
|
||||
* **dialogs:** fix footer position on dialogs ([#470](https://github.com/chartdb/chartdb/issues/470)) ([2309306](https://github.com/chartdb/chartdb/commit/2309306ef590783b00a2489209092107dd9a3788))
|
||||
* **sql-server import:** nullable should be boolean instead of string ([#480](https://github.com/chartdb/chartdb/issues/480)) ([635fb53](https://github.com/chartdb/chartdb/commit/635fb53c9f7ebd1e5ef4d9274af041edc08f04c3))
|
||||
|
||||
## [1.4.0](https://github.com/chartdb/chartdb/compare/v1.3.1...v1.4.0) (2024-12-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **add templates:** add six more templates ([#452](https://github.com/chartdb/chartdb/issues/452)) ([be1b109](https://github.com/chartdb/chartdb/commit/be1b109f23e62df4cc63fa8914c2754f7809cc08))
|
||||
* **add templates:** add six more templates (django-axes, laravel-activitylog, octobox, pay-rails, pixelfed, polr) ([#460](https://github.com/chartdb/chartdb/issues/460)) ([03772f6](https://github.com/chartdb/chartdb/commit/03772f6b4f99f9c4350356aa0f2a4666f4f1794d))
|
||||
* **add templates:** add six more templates (reversion, screeenly, staytus, deployer, devise, talk) ([#457](https://github.com/chartdb/chartdb/issues/457)) ([ddeef3b](https://github.com/chartdb/chartdb/commit/ddeef3b134efa893e1c1e15e2f87c27157200e2d))
|
||||
* **clickhouse:** add ClickHouse support ([#463](https://github.com/chartdb/chartdb/issues/463)) ([807cd22](https://github.com/chartdb/chartdb/commit/807cd22e0c739f339fa07fe1d2f043c5411ae41f))
|
||||
* **i18n:** Added bangla translations ([#432](https://github.com/chartdb/chartdb/issues/432)) ([885eb71](https://github.com/chartdb/chartdb/commit/885eb719de577c2652fbed1ed287f38fcc98c148))
|
||||
* **side-panel:** Add functionality of order tables by drag & drop ([#425](https://github.com/chartdb/chartdb/issues/425)) ([a0e966b](https://github.com/chartdb/chartdb/commit/a0e966b64f8070d4595d47b2fb39e8bbf427b794))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **clipboard:** defensive for navigator clipboard ([#462](https://github.com/chartdb/chartdb/issues/462)) ([5fc10a7](https://github.com/chartdb/chartdb/commit/5fc10a7e649fc5877bb297b519b1b6a8b81f1323))
|
||||
* **import-database:** update database type after importing into an existing generic diagra ([#456](https://github.com/chartdb/chartdb/issues/456)) ([a8fe491](https://github.com/chartdb/chartdb/commit/a8fe491c1b5a30d9f4144cefa9111dd3dfd5df1a))
|
||||
* **Last Saved:** Translate the "last saved" relative date message ([#400](https://github.com/chartdb/chartdb/issues/400)) ([d45677e](https://github.com/chartdb/chartdb/commit/d45677e92d72efc6cea8f865ce46f0be6ec9961f))
|
||||
* **mariadb-types:** Add uuid data type ([#459](https://github.com/chartdb/chartdb/issues/459)) ([94656ec](https://github.com/chartdb/chartdb/commit/94656ec7a5435c2da262fb3bc6a6d381d554b0c1))
|
||||
* window type ([#454](https://github.com/chartdb/chartdb/issues/454)) ([9c7d03c](https://github.com/chartdb/chartdb/commit/9c7d03c285ff6f818eef3199c9b7a530d03a1fec))
|
||||
|
||||
## [1.3.1](https://github.com/chartdb/chartdb/compare/v1.3.0...v1.3.1) (2024-11-26)
|
||||
|
||||
|
||||
|
||||
45
CLA.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# ChartDB Contributors License Agreement
|
||||
|
||||
This Contributors License Agreement ("CLA") is entered into between the Contributor, and ChartDB, Inc. ("ChartDB"), collectively referred to as the "Parties."
|
||||
|
||||
## Background:
|
||||
|
||||
ChartDB is an open-source project aimed at providing an open-source database diagramming and visualization tool for all parties.This CLA governs the rights and contributions made by the Contributor to the ChartDB project.
|
||||
|
||||
## Agreement:
|
||||
|
||||
**Contributor Grant of License:**
|
||||
|
||||
By submitting code, documentation, or any other materials (collectively, "Contributions") to the ChartDB project, the Contributor grants ChartDB a perpetual, worldwide, non-exclusive, royalty-free, sublicensable license to use, modify, distribute, and otherwise exploit the Contributions, including any intellectual property rights therein, for the purposes of the ChartDB project.
|
||||
|
||||
**Representation of Ownership and Right to Contribute:**
|
||||
|
||||
The Contributor represents that they have the legal right to grant the license stated in Section 1, and that the Contributions do not infringe upon the intellectual property rights of any third party. The Contributor also represents that they have the authority to submit the Contributions on their own behalf or, if applicable, on behalf of their employer or any other entity.
|
||||
|
||||
**Patent Grant:**
|
||||
|
||||
If the Contributions include any method, process, or apparatus that is covered by a patent, the Contributor agrees to grant ChartDB a non-exclusive, worldwide, royalty-free license under any patent claims necessary to use, modify, distribute, and otherwise exploit the Contributions for the purposes of the ChartDB project.
|
||||
|
||||
**No Implied Warranties or Support:**
|
||||
|
||||
The Contributor acknowledges that the Contributions are provided "as is," without any warranties or support of any kind. ChartDB shall have no obligation to provide maintenance, updates, bug fixes, or support for the Contributions.
|
||||
|
||||
**Retention of Contributor Rights:**
|
||||
|
||||
The Contributor retains all right, title, and interest in and to their Contributions. This CLA does not restrict the Contributor from using their own Contributions for any other purpose.
|
||||
|
||||
**Governing Law:**
|
||||
|
||||
This CLA shall be governed by and construed in accordance with the laws of Delaware (DE), without regard to its conflict of laws principles.
|
||||
|
||||
**Entire Agreement:**
|
||||
|
||||
This CLA constitutes the entire agreement between the Parties with respect to the subject matter hereof and supersedes all prior and contemporaneous understandings, agreements, representations, and warranties.
|
||||
|
||||
**Acceptance:**
|
||||
|
||||
By submitting Contributions to the ChartDB project, the Contributor acknowledges and agrees to the terms and conditions of this CLA. If the Contributor is agreeing to this CLA on behalf of an entity, they represent that they have the necessary authority to bind that entity to these terms.
|
||||
|
||||
**Effective Date:**
|
||||
|
||||
This CLA is effective as of the date of the first Contribution made by the Contributor to the ChartDB project.
|
||||
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
chartdb.io@gmail.com.
|
||||
support@chartdb.io.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
||||
@@ -18,7 +18,7 @@ To submit a pull request:
|
||||
|
||||
If you find a bug, check [GitHub issues](https://github.com/chartdb/chartdb/issues) to see if it’s already reported. If not, feel free to [report it](https://github.com/chartdb/chartdb/issues/new?labels=bug).
|
||||
|
||||
For questions about using ChartDB, reach out to us via Email (chartdb.io@gmail.com) or [Discord](https://discord.gg/QeFwyWSKwC). For feature requests, create a [new feature](https://github.com/chartdb/chartdb/issues/new?labels=enhancement).
|
||||
For questions about using ChartDB, reach out to us via Email (support@chartdb.io) or [Discord](https://discord.gg/QeFwyWSKwC). For feature requests, create a [new feature](https://github.com/chartdb/chartdb/issues/new?labels=enhancement).
|
||||
|
||||
### Creating a Branch
|
||||
|
||||
@@ -35,7 +35,7 @@ By contributing, you agree that your work will be licensed under ChartDB's [lice
|
||||
## Questions?
|
||||
|
||||
Feel free to ask in `#contributing` on [Discord](https://discord.gg/QeFwyWSKwC) if you have questions about our process, how to proceed, etc.
|
||||
or [Email](chartdb.io@gmail.com)
|
||||
or [Email](support@chartdb.io)
|
||||
|
||||
---
|
||||
|
||||
|
||||
14
Dockerfile
@@ -1,6 +1,10 @@
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
ARG VITE_OPENAI_API_KEY
|
||||
ARG VITE_OPENAI_API_ENDPOINT
|
||||
ARG VITE_LLM_MODEL_NAME
|
||||
ARG VITE_HIDE_CHARTDB_CLOUD
|
||||
ARG VITE_DISABLE_ANALYTICS
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -10,9 +14,14 @@ RUN npm ci
|
||||
|
||||
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_CHARTDB_CLOUD=${VITE_HIDE_CHARTDB_CLOUD}" >> .env && \
|
||||
echo "VITE_DISABLE_ANALYTICS=${VITE_DISABLE_ANALYTICS}" >> .env
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Use a lightweight web server to serve the production build
|
||||
FROM nginx:stable-alpine AS production
|
||||
|
||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
||||
@@ -20,7 +29,6 @@ COPY ./default.conf.template /etc/nginx/conf.d/default.conf.template
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Expose the default port for the Nginx web server
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
74
README.md
@@ -30,8 +30,8 @@
|
||||
<a href="https://discord.gg/QeFwyWSKwC">
|
||||
<img src="https://img.shields.io/discord/1277047413705670678?color=5865F2&label=Discord&logo=discord&logoColor=white" alt="Discord community channel" />
|
||||
</a>
|
||||
<a href="https://x.com/chartdb_io">
|
||||
<img src="https://img.shields.io/twitter/follow/ChartDB?style=social"/>
|
||||
<a href="https://x.com/intent/follow?screen_name=jonathanfishner">
|
||||
<img src="https://img.shields.io/twitter/follow/jonathanfishner?style=social"/>
|
||||
</a>
|
||||
|
||||
</h4>
|
||||
@@ -49,13 +49,13 @@ Instantly visualize your database schema with a single **"Smart Query."** Custom
|
||||
|
||||
**What it does**:
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
- **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.
|
||||
- **Interactive Editing**
|
||||
Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures.
|
||||
- **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.
|
||||
- **Interactive Editing**
|
||||
Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures.
|
||||
|
||||
### Status
|
||||
|
||||
@@ -63,12 +63,13 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
|
||||
|
||||
### 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"/> )
|
||||
- ✅ MySQL
|
||||
- ✅ SQL Server
|
||||
- ✅ MariaDB
|
||||
- ✅ SQLite
|
||||
- ✅ ClickHouse
|
||||
- ✅ 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
|
||||
- ✅ SQL Server
|
||||
- ✅ MariaDB
|
||||
- ✅ 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
|
||||
- ✅ ClickHouse
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -90,24 +91,53 @@ npm run build
|
||||
|
||||
Or like this if you want to have AI capabilities:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install
|
||||
VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
|
||||
```
|
||||
|
||||
### Running the Docker Container
|
||||
### Run the Docker Container
|
||||
|
||||
```bash
|
||||
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest
|
||||
```
|
||||
|
||||
#### Build & run Docker image locally
|
||||
#### Build and Run locally
|
||||
|
||||
```bash
|
||||
docker build -t chartdb . (If you want AI capabilities, use `docker build --build-arg VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -t chartdb .`)
|
||||
docker run -p 8080:80 chartdb
|
||||
docker build -t 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
|
||||
```
|
||||
|
||||
> **Privacy Note:** ChartDB includes privacy-focused analytics via Fathom Analytics. You can disable this by adding `-e DISABLE_ANALYTICS=true` to the run command or `--build-arg VITE_DISABLE_ANALYTICS=true` when building.
|
||||
|
||||
> **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`.
|
||||
|
||||
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
|
||||
|
||||
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
|
||||
@@ -119,9 +149,9 @@ Open your browser and navigate to `http://localhost:8080`.
|
||||
|
||||
## 💚 Community & Support
|
||||
|
||||
- [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)
|
||||
- [Twitter](https://x.com/chartdb_io) (Get news fast)
|
||||
- [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)
|
||||
- [Twitter](https://x.com/intent/follow?screen_name=jonathanfishner) (Get news fast)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "src/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "src/components",
|
||||
"utils": "src/lib/utils",
|
||||
"ui": "src/components/ui",
|
||||
"lib": "src/lib",
|
||||
"hooks": "src/hooks"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@ server {
|
||||
|
||||
location /config.js {
|
||||
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_CHARTDB_CLOUD: \"$HIDE_CHARTDB_CLOUD\",
|
||||
DISABLE_ANALYTICS: \"$DISABLE_ANALYTICS\"
|
||||
};";
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 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_CHARTDB_CLOUD} ${DISABLE_ANALYTICS}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Start Nginx
|
||||
nginx -g "daemon off;"
|
||||
|
||||
77
eslint.config.mjs
Normal file
@@ -0,0 +1,77 @@
|
||||
import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import cssModules from 'eslint-plugin-css-modules';
|
||||
import tailwindcss from 'eslint-plugin-tailwindcss';
|
||||
import jsxA11Y from 'eslint-plugin-jsx-a11y';
|
||||
import globals from 'globals';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import js from '@eslint/js';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/dist', '**/.eslintrc.cjs', '**/tailwind.config.js'],
|
||||
// files: ['**/*.ts', '**/*.tsx'],
|
||||
},
|
||||
...fixupConfigRules(
|
||||
compat.extends(
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:css-modules/recommended',
|
||||
'plugin:tailwindcss/recommended',
|
||||
'plugin:prettier/recommended'
|
||||
)
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
'react-refresh': reactRefresh,
|
||||
'css-modules': fixupPluginRules(cssModules),
|
||||
tailwindcss: fixupPluginRules(tailwindcss),
|
||||
'jsx-a11y': jsxA11Y,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
// parserOptions: {
|
||||
// project: './tsconfig.json',
|
||||
// },
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{
|
||||
allowConstantExport: true,
|
||||
},
|
||||
],
|
||||
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
20
index.html
@@ -13,11 +13,21 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="/config.js"></script>
|
||||
<script
|
||||
src="https://cdn.usefathom.com/script.js"
|
||||
data-site="PRHIVBNN"
|
||||
defer
|
||||
></script>
|
||||
<script>
|
||||
// Load analytics only if not disabled
|
||||
(function() {
|
||||
const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
|
||||
(typeof process !== 'undefined' && process.env && process.env.VITE_DISABLE_ANALYTICS === 'true');
|
||||
|
||||
if (!disableAnalytics) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.usefathom.com/script.js';
|
||||
script.setAttribute('data-site', 'PRHIVBNN');
|
||||
script.defer = true;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
22051
package-lock.json
generated
44
package.json
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"private": true,
|
||||
"version": "1.3.1",
|
||||
"version": "1.13.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "npm run lint && tsc -b && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"preview": "vite preview",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.51",
|
||||
"@dbml/core": "^3.9.5",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
@@ -21,27 +25,27 @@
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@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-hover-card": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-menubar": "^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-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^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",
|
||||
"@xyflow/react": "^12.3.1",
|
||||
"ahooks": "^3.8.1",
|
||||
"ai": "^3.3.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"dexie": "^4.0.8",
|
||||
@@ -60,7 +64,7 @@
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-resizable-panels": "^2.0.22",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-router-dom": "^7.1.1",
|
||||
"react-use": "^17.5.1",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@@ -69,22 +73,31 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.4",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"eslint-plugin-tailwindcss": "^3.17.4",
|
||||
"globals": "^15.13.0",
|
||||
"happy-dom": "^18.0.1",
|
||||
"husky": "^9.1.5",
|
||||
"postcss": "^8.4.40",
|
||||
"prettier": "^3.3.3",
|
||||
@@ -92,6 +105,7 @@
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.2.2",
|
||||
"unplugin-inject-preload": "^3.0.0",
|
||||
"vite": "^5.3.4"
|
||||
"vite": "^5.3.4",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
166
parser-comparison-analysis.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# PostgreSQL vs MySQL Parser Comparison Analysis
|
||||
|
||||
## Overview
|
||||
This document compares how the PostgreSQL and MySQL parsers in ChartDB handle SQL parsing, focusing on the differences that could cause the same SQL file to produce different results.
|
||||
|
||||
## 1. SQL Sanitization and Comment Handling
|
||||
|
||||
### PostgreSQL Parser (`postgresql-improved.ts`)
|
||||
|
||||
#### Comment Removal Strategy:
|
||||
1. **Order**: Comments are removed FIRST, before any other processing
|
||||
2. **Multi-line comments**: Removed using regex: `/\/\*[\s\S]*?\*\//g`
|
||||
3. **Single-line comments**: Removed line-by-line, checking for `--` while respecting string boundaries
|
||||
4. **String-aware**: Preserves `--` inside quoted strings
|
||||
|
||||
```typescript
|
||||
// PostgreSQL approach (lines 60-100)
|
||||
// 1. First removes ALL multi-line comments
|
||||
cleanedSQL = cleanedSQL.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
|
||||
// 2. Then processes single-line comments while respecting strings
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
// Tracks if we're inside a string to avoid removing -- inside quotes
|
||||
}
|
||||
```
|
||||
|
||||
### MySQL Parser (`mysql-improved.ts`)
|
||||
|
||||
#### Comment Removal Strategy:
|
||||
1. **Order**: Comments are sanitized but with special handling for problematic patterns
|
||||
2. **Special handling**: Specifically fixes multi-line comments that contain quotes or JSON
|
||||
3. **Line-by-line**: Processes comments line by line, removing lines that start with `--` or `#`
|
||||
|
||||
```typescript
|
||||
// MySQL approach (lines 35-67)
|
||||
// 1. First fixes specific problematic patterns
|
||||
result = result.replace(/--\s*"[^"]*",?\s*\n\s*"[^"]*".*$/gm, function(match) {
|
||||
return match.replace(/\n/g, ' ');
|
||||
});
|
||||
|
||||
// 2. Then removes comment lines entirely
|
||||
.map((line) => {
|
||||
if (trimmed.startsWith('--') || trimmed.startsWith('#')) {
|
||||
return '';
|
||||
}
|
||||
return line;
|
||||
})
|
||||
```
|
||||
|
||||
**Key Difference**: PostgreSQL removes ALL comments upfront, while MySQL tries to fix problematic comment patterns first, then removes comment lines.
|
||||
|
||||
## 2. Order of Operations
|
||||
|
||||
### PostgreSQL Parser
|
||||
1. **Preprocess SQL** (removes all comments first)
|
||||
2. **Split statements** by semicolons (handles dollar quotes)
|
||||
3. **Categorize statements** (table, index, alter, etc.)
|
||||
4. **Parse with node-sql-parser**
|
||||
5. **Fallback to regex** if parser fails
|
||||
6. **Extract relationships**
|
||||
|
||||
### MySQL Parser
|
||||
1. **Validate syntax** (checks for known issues)
|
||||
2. **Sanitize SQL** (fixes problematic patterns)
|
||||
3. **Extract statements** by semicolons
|
||||
4. **Parse with node-sql-parser**
|
||||
5. **Fallback to regex** if parser fails
|
||||
6. **Process relationships**
|
||||
|
||||
**Key Difference**: MySQL validates BEFORE sanitizing, while PostgreSQL sanitizes first. This means MySQL can detect and report issues that PostgreSQL might silently fix.
|
||||
|
||||
## 3. Multi-line Comment Handling
|
||||
|
||||
### PostgreSQL
|
||||
- Removes ALL multi-line comments using `[\s\S]*?` pattern
|
||||
- No special handling for comments containing quotes or JSON
|
||||
- Clean removal before any parsing
|
||||
|
||||
### MySQL
|
||||
- Specifically detects and fixes multi-line comments with quotes:
|
||||
```sql
|
||||
-- "Beliebt",
|
||||
"Empfohlen" -- This breaks MySQL parser
|
||||
```
|
||||
- Detects JSON arrays in comments spanning lines:
|
||||
```sql
|
||||
-- [
|
||||
"Ubuntu 22.04",
|
||||
"CentOS 8"
|
||||
] -- This also breaks MySQL parser
|
||||
```
|
||||
- Converts these to single-line comments before parsing
|
||||
|
||||
**Key Difference**: MySQL has specific handling for problematic comment patterns that PostgreSQL simply removes entirely.
|
||||
|
||||
## 4. Statement Splitting
|
||||
|
||||
### PostgreSQL
|
||||
- Handles PostgreSQL-specific dollar quotes (`$$ ... $$`)
|
||||
- Tracks quote depth for proper splitting
|
||||
- Supports function bodies with dollar quotes
|
||||
|
||||
### MySQL
|
||||
- Simple quote tracking (single, double, backtick)
|
||||
- Handles escape sequences (`\`)
|
||||
- No special quote constructs
|
||||
|
||||
## 5. Validation Approach
|
||||
|
||||
### PostgreSQL
|
||||
- No pre-validation
|
||||
- Relies on parser and fallback regex
|
||||
- Reports warnings for unsupported features
|
||||
|
||||
### MySQL
|
||||
- Pre-validates SQL before parsing
|
||||
- Detects known problematic patterns:
|
||||
- Multi-line comments with quotes
|
||||
- JSON arrays in comments
|
||||
- Inline REFERENCES (PostgreSQL syntax)
|
||||
- Missing semicolons
|
||||
- Can reject SQL before attempting to parse
|
||||
|
||||
## 6. Why Same SQL Gives Different Results
|
||||
|
||||
### Example Problematic SQL:
|
||||
```sql
|
||||
CREATE TABLE products (
|
||||
id INT PRIMARY KEY,
|
||||
status VARCHAR(50), -- "active",
|
||||
"inactive", "pending"
|
||||
data JSON -- [
|
||||
{"key": "value"},
|
||||
{"key": "value2"}
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
### PostgreSQL Result:
|
||||
- Successfully parses (comments are removed entirely)
|
||||
- Table created with proper columns
|
||||
|
||||
### MySQL Result:
|
||||
- Validation fails with errors:
|
||||
- MULTILINE_COMMENT_QUOTE at line 3
|
||||
- MULTILINE_JSON_COMMENT at line 5
|
||||
- Import blocked unless validation is skipped
|
||||
|
||||
## 7. Recommendations
|
||||
|
||||
1. **For Cross-Database Compatibility**:
|
||||
- Avoid multi-line comments with quotes or JSON
|
||||
- Keep comments on single lines
|
||||
- Use proper FOREIGN KEY syntax instead of inline REFERENCES
|
||||
|
||||
2. **For MySQL Import**:
|
||||
- Fix validation errors before import
|
||||
- Or use `skipValidation: true` option if SQL is known to work
|
||||
|
||||
3. **For PostgreSQL Import**:
|
||||
- Be aware that comments are stripped entirely
|
||||
- Complex comments might hide syntax issues
|
||||
|
||||
## Conclusion
|
||||
|
||||
The main difference is that PostgreSQL takes a "remove all comments first" approach, while MySQL tries to detect and handle problematic comment patterns. This makes PostgreSQL more forgiving but MySQL more explicit about potential issues. The same SQL file can succeed in PostgreSQL but fail in MySQL if it contains multi-line comments with special characters.
|
||||
BIN
public/buckle-animated.gif
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
public/buckle.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/clickhouse_logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/clickhouse_logo_2.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/clickhouse_logo_dark.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
src/assets/cloudflare_d1.png
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
src/assets/cockroachdb_logo.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/assets/cockroachdb_logo_2.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
src/assets/cockroachdb_logo_dark.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/empty_state_dark.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 441 KiB After Width: | Height: | Size: 543 KiB |
|
Before Width: | Height: | Size: 405 KiB After Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 359 KiB |
BIN
src/assets/oracle_logo.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/assets/oracle_logo_2.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/oracle_logo_dark.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/assets/templates/cachet-db-dark.png
Normal file
|
After Width: | Height: | Size: 447 KiB |
BIN
src/assets/templates/cachet-db.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
src/assets/templates/canvas-db-dark.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
src/assets/templates/canvas-db.png
Normal file
|
After Width: | Height: | Size: 379 KiB |
BIN
src/assets/templates/deployer-db-dark.png
Normal file
|
After Width: | Height: | Size: 424 KiB |
BIN
src/assets/templates/deployer-db.png
Normal file
|
After Width: | Height: | Size: 497 KiB |
BIN
src/assets/templates/devise-db-dark.png
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
src/assets/templates/devise-db.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
src/assets/templates/django-axes-db-dark.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
src/assets/templates/django-axes-db.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
src/assets/templates/doorkeeper-db-dark.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
src/assets/templates/doorkeeper-db.png
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
src/assets/templates/flipper-db-dark.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
src/assets/templates/flipper-db.png
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
src/assets/templates/laravel-activitylog-db-dark.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
src/assets/templates/laravel-activitylog-db.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
src/assets/templates/octobox-db-dark.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
src/assets/templates/octobox-db.png
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
src/assets/templates/orchid-db-dark.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
src/assets/templates/orchid-db.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
src/assets/templates/pay-rails-db-dark.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
src/assets/templates/pay-rails-db.png
Normal file
|
After Width: | Height: | Size: 371 KiB |
BIN
src/assets/templates/pixelfed-db-dark.png
Normal file
|
After Width: | Height: | Size: 593 KiB |
BIN
src/assets/templates/pixelfed-db.png
Normal file
|
After Width: | Height: | Size: 687 KiB |
BIN
src/assets/templates/polr-db-dark.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
src/assets/templates/polr-db.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
src/assets/templates/reversion-db-dark.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
src/assets/templates/reversion-db.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
src/assets/templates/screeenly-db-dark.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
src/assets/templates/screeenly-db.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
src/assets/templates/staytus-db-dark.png
Normal file
|
After Width: | Height: | Size: 424 KiB |
BIN
src/assets/templates/staytus-db.png
Normal file
|
After Width: | Height: | Size: 471 KiB |
BIN
src/assets/templates/taggit-db-dark.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
src/assets/templates/taggit-db.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
src/assets/templates/talk-db-dark.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
src/assets/templates/talk-db.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
@@ -1,2 +1,3 @@
|
||||
import './config.ts';
|
||||
export { Editor } from '@monaco-editor/react';
|
||||
export { DiffEditor } from '@monaco-editor/react';
|
||||
|
||||
@@ -3,7 +3,9 @@ import React, { lazy, Suspense, useCallback, useEffect } from 'react';
|
||||
import { Spinner } from '../spinner/spinner';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { useMonaco } from '@monaco-editor/react';
|
||||
import { useToast } from '@/components/toast/use-toast';
|
||||
import { Button } from '../button/button';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Copy, CopyCheck } from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -11,33 +13,54 @@ import { DarkTheme } from './themes/dark';
|
||||
import { LightTheme } from './themes/light';
|
||||
import './config.ts';
|
||||
|
||||
export interface CodeSnippetProps {
|
||||
className?: string;
|
||||
code: string;
|
||||
language?: 'sql' | 'shell';
|
||||
loading?: boolean;
|
||||
autoScroll?: boolean;
|
||||
isComplete?: boolean;
|
||||
}
|
||||
|
||||
export const Editor = lazy(() =>
|
||||
import('./code-editor').then((module) => ({
|
||||
default: module.Editor,
|
||||
}))
|
||||
);
|
||||
|
||||
export const DiffEditor = lazy(() =>
|
||||
import('./code-editor').then((module) => ({
|
||||
default: module.DiffEditor,
|
||||
}))
|
||||
);
|
||||
|
||||
type EditorType = typeof Editor;
|
||||
|
||||
export interface CodeSnippetAction {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export interface CodeSnippetProps {
|
||||
className?: string;
|
||||
code: string;
|
||||
codeToCopy?: string;
|
||||
language?: 'sql' | 'shell';
|
||||
loading?: boolean;
|
||||
autoScroll?: boolean;
|
||||
isComplete?: boolean;
|
||||
editorProps?: React.ComponentProps<EditorType>;
|
||||
actions?: CodeSnippetAction[];
|
||||
}
|
||||
|
||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
({
|
||||
className,
|
||||
code,
|
||||
codeToCopy,
|
||||
loading,
|
||||
language = 'sql',
|
||||
autoScroll = false,
|
||||
isComplete = true,
|
||||
editorProps,
|
||||
actions,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const monaco = useMonaco();
|
||||
const { effectiveTheme } = useTheme();
|
||||
const { toast } = useToast();
|
||||
const [isCopied, setIsCopied] = React.useState(false);
|
||||
const [tooltipOpen, setTooltipOpen] = React.useState(false);
|
||||
|
||||
@@ -66,10 +89,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
}
|
||||
}, [code, monaco, autoScroll]);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
navigator.clipboard.writeText(code);
|
||||
setIsCopied(true);
|
||||
}, [code]);
|
||||
const copyToClipboard = useCallback(async () => {
|
||||
if (!navigator?.clipboard) {
|
||||
toast({
|
||||
title: t('copy_to_clipboard_toast.unsupported.title'),
|
||||
variant: 'destructive',
|
||||
description: t(
|
||||
'copy_to_clipboard_toast.unsupported.description'
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeToCopy ?? code);
|
||||
setIsCopied(true);
|
||||
} catch {
|
||||
setIsCopied(false);
|
||||
toast({
|
||||
title: t('copy_to_clipboard_toast.failed.title'),
|
||||
variant: 'destructive',
|
||||
description: t(
|
||||
'copy_to_clipboard_toast.failed.description'
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [code, codeToCopy, t, toast]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -83,36 +128,58 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
) : (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{isComplete ? (
|
||||
<Tooltip
|
||||
onOpenChange={setTooltipOpen}
|
||||
open={isCopied || tooltipOpen}
|
||||
>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
className="absolute right-1 top-1 z-10"
|
||||
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
|
||||
<Tooltip
|
||||
onOpenChange={setTooltipOpen}
|
||||
open={isCopied || tooltipOpen}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
className=" h-fit p-1.5"
|
||||
variant="outline"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CopyCheck size={16} />
|
||||
) : (
|
||||
<Copy size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t(
|
||||
isCopied
|
||||
? 'copied'
|
||||
: 'copy_to_clipboard'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
className="h-fit p-1.5"
|
||||
variant="outline"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CopyCheck size={16} />
|
||||
) : (
|
||||
<Copy size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t(
|
||||
isCopied
|
||||
? 'copied'
|
||||
: 'copy_to_clipboard'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{actions &&
|
||||
actions.length > 0 &&
|
||||
actions.map((action, index) => (
|
||||
<Tooltip key={index}>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
className="h-fit p-1.5"
|
||||
variant="outline"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<action.icon
|
||||
size={16}
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{action.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Editor
|
||||
@@ -120,27 +187,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
language={language}
|
||||
loading={<Spinner />}
|
||||
theme={effectiveTheme}
|
||||
{...editorProps}
|
||||
options={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
readOnly: true,
|
||||
automaticLayout: true,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
renderValidationDecorations: 'off',
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
contextmenu: false,
|
||||
...editorProps?.options,
|
||||
guides: {
|
||||
indentation: false,
|
||||
...editorProps?.options?.guides,
|
||||
},
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
alwaysConsumeMouseWheel: false,
|
||||
...editorProps?.options?.scrollbar,
|
||||
},
|
||||
minimap: {
|
||||
enabled: false,
|
||||
...editorProps?.options?.minimap,
|
||||
},
|
||||
contextmenu: false,
|
||||
}}
|
||||
/>
|
||||
{!isComplete ? (
|
||||
|
||||
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,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
|
||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import {
|
||||
databaseEditionToImageMap,
|
||||
databaseEditionToLabelMap,
|
||||
@@ -9,39 +9,46 @@ import {
|
||||
databaseSecondaryLogoMap,
|
||||
databaseTypeToLabelMap,
|
||||
} from '@/lib/databases';
|
||||
import type { DatabaseType } from '@/lib/domain/database-type';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface DiagramIconProps {
|
||||
diagram: Diagram;
|
||||
export interface DiagramIconProps
|
||||
extends React.ComponentPropsWithoutRef<'div'> {
|
||||
databaseType: DatabaseType;
|
||||
databaseEdition?: DatabaseEdition;
|
||||
imgClassName?: string;
|
||||
}
|
||||
|
||||
export const DiagramIcon = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipTrigger>,
|
||||
DiagramIconProps
|
||||
>(({ diagram }, ref) =>
|
||||
diagram.databaseEdition ? (
|
||||
>(({ databaseType, databaseEdition, className, imgClassName, onClick }, ref) =>
|
||||
databaseEdition ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="mr-1" ref={ref}>
|
||||
<TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
|
||||
<img
|
||||
src={databaseEditionToImageMap[diagram.databaseEdition]}
|
||||
className="h-5 max-w-fit rounded-full"
|
||||
src={databaseEditionToImageMap[databaseEdition]}
|
||||
className={cn('max-h-5 max-w-5 rounded-full', imgClassName)}
|
||||
alt="database"
|
||||
onClick={onClick}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{databaseEditionToLabelMap[diagram.databaseEdition]}
|
||||
{databaseEditionToLabelMap[databaseEdition]}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="mr-2" ref={ref}>
|
||||
<TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
|
||||
<img
|
||||
src={databaseSecondaryLogoMap[diagram.databaseType]}
|
||||
className="h-5 max-w-fit"
|
||||
src={databaseSecondaryLogoMap[databaseType]}
|
||||
className={cn('max-h-5 max-w-5', imgClassName)}
|
||||
alt="database"
|
||||
onClick={onClick}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{databaseTypeToLabelMap[diagram.databaseType]}
|
||||
{databaseTypeToLabelMap[databaseType]}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -117,7 +117,10 @@ const DialogInternalContent = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ScrollArea
|
||||
ref={ref}
|
||||
className={cn('flex max-h-screen flex-col overflow-y-auto', className)}
|
||||
className={cn(
|
||||
'flex flex-1 max-h-screen flex-col overflow-y-auto',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -1,30 +1,66 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import EmptyStateImage from '@/assets/empty_state.png';
|
||||
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
|
||||
import { Label } from '@/components/label/label';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
|
||||
export interface EmptyStateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
imageClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
}
|
||||
|
||||
export const EmptyState = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & EmptyStateProps
|
||||
>(({ title, description, className }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col items-center justify-center space-y-1',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<img src={EmptyStateImage} alt="Empty state" className="w-32" />
|
||||
<Label className="text-base">{title}</Label>
|
||||
<Label className="text-sm font-normal text-muted-foreground">
|
||||
{description}
|
||||
</Label>
|
||||
</div>
|
||||
));
|
||||
>(
|
||||
(
|
||||
{
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
titleClassName,
|
||||
descriptionClassName,
|
||||
imageClassName,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { effectiveTheme } = useTheme();
|
||||
|
||||
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';
|
||||
|
||||
@@ -24,12 +24,20 @@ export interface SelectBoxOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
regex?: string;
|
||||
extractRegex?: RegExp;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
export interface SelectBoxProps {
|
||||
options: SelectBoxOption[];
|
||||
value?: string[] | string;
|
||||
onChange?: (values: string[] | string) => void;
|
||||
valueSuffix?: string;
|
||||
optionSuffix?: (option: SelectBoxOption) => string;
|
||||
onChange?: (
|
||||
values: string[] | string,
|
||||
regexMatches?: string[] | string
|
||||
) => void;
|
||||
placeholder?: string;
|
||||
inputPlaceholder?: string;
|
||||
emptyPlaceholder?: string;
|
||||
@@ -44,6 +52,7 @@ export interface SelectBoxProps {
|
||||
disabled?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
popoverClassName?: string;
|
||||
}
|
||||
|
||||
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
@@ -55,10 +64,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
className,
|
||||
options,
|
||||
value,
|
||||
valueSuffix,
|
||||
onChange,
|
||||
multiple,
|
||||
oneLine,
|
||||
selectAll,
|
||||
optionSuffix,
|
||||
deselectAll,
|
||||
clearText,
|
||||
showClear,
|
||||
@@ -66,6 +77,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
disabled,
|
||||
open,
|
||||
onOpenChange: setOpen,
|
||||
popoverClassName,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -86,7 +98,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
);
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(selectedValue: string) => {
|
||||
(selectedValue: string, regexMatches?: string[]) => {
|
||||
if (multiple) {
|
||||
const newValue =
|
||||
value?.includes(selectedValue) && Array.isArray(value)
|
||||
@@ -94,7 +106,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
: [...(value ?? []), selectedValue];
|
||||
onChange?.(newValue);
|
||||
} else {
|
||||
onChange?.(selectedValue);
|
||||
onChange?.(selectedValue, regexMatches);
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
@@ -166,6 +178,101 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
[isOpen, onOpenChange]
|
||||
);
|
||||
|
||||
const groups = React.useMemo(
|
||||
() =>
|
||||
options.reduce(
|
||||
(acc, option) => {
|
||||
if (option.group) {
|
||||
if (!acc[option.group]) {
|
||||
acc[option.group] = [];
|
||||
}
|
||||
acc[option.group].push(option);
|
||||
} else {
|
||||
if (!acc['default']) {
|
||||
acc['default'] = [];
|
||||
}
|
||||
acc['default'].push(option);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, SelectBoxOption[]>
|
||||
),
|
||||
[options]
|
||||
);
|
||||
|
||||
const hasGroups = React.useMemo(
|
||||
() =>
|
||||
Object.keys(groups).filter((group) => group !== 'default')
|
||||
.length > 0,
|
||||
[groups]
|
||||
);
|
||||
|
||||
const renderOption = React.useCallback(
|
||||
(option: SelectBoxOption) => {
|
||||
const isSelected =
|
||||
Array.isArray(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 (
|
||||
<CommandItem
|
||||
className="flex items-center"
|
||||
key={option.value}
|
||||
keywords={option.regex ? [option.regex] : undefined}
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
option.value,
|
||||
matches?.map((match) => match.toString())
|
||||
)
|
||||
}
|
||||
>
|
||||
{multiple && (
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-1 items-center truncate">
|
||||
<span>
|
||||
{isRegexMatch ? searchTerm : option.label}
|
||||
{!isRegexMatch && optionSuffix
|
||||
? optionSuffix(option)
|
||||
: ''}
|
||||
</span>
|
||||
{option.description && (
|
||||
<span className="ml-1 w-0 flex-1 truncate text-xs text-muted-foreground">
|
||||
{option.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{((!multiple && option.value === value) ||
|
||||
isRegexMatch) && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value === value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
},
|
||||
[value, multiple, searchTerm, handleSelect, optionSuffix]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
|
||||
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
|
||||
@@ -199,6 +306,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
(opt) => opt.value === value
|
||||
)?.label
|
||||
}
|
||||
{valueSuffix ? valueSuffix : ''}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
@@ -235,15 +343,29 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-fit min-w-[var(--radix-popover-trigger-width)] p-0"
|
||||
className={cn(
|
||||
'w-fit min-w-[var(--radix-popover-trigger-width)] p-0',
|
||||
popoverClassName
|
||||
)}
|
||||
align="center"
|
||||
>
|
||||
<Command
|
||||
filter={(value, search) =>
|
||||
value.toLowerCase().includes(search.toLowerCase())
|
||||
filter={(value, search, keywords) => {
|
||||
if (
|
||||
keywords?.length &&
|
||||
keywords.some((keyword) =>
|
||||
new RegExp(keyword).test(search)
|
||||
)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return value
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase())
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
: 0;
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
@@ -296,65 +418,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
|
||||
<ScrollArea>
|
||||
<div className="max-h-64 w-full">
|
||||
<CommandGroup>
|
||||
<CommandList className="max-h-fit w-full">
|
||||
{options.map((option) => {
|
||||
const isSelected =
|
||||
Array.isArray(value) &&
|
||||
value.includes(option.value);
|
||||
return (
|
||||
<CommandItem
|
||||
className="flex items-center"
|
||||
key={option.value}
|
||||
// value={option.value}
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
option.value
|
||||
)
|
||||
}
|
||||
>
|
||||
{multiple && (
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center truncate">
|
||||
<span>
|
||||
{option.label}
|
||||
</span>
|
||||
{option.description && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
{
|
||||
option.description
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!multiple &&
|
||||
option.value ===
|
||||
value && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value ===
|
||||
value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
<CommandList className="max-h-fit w-full">
|
||||
{hasGroups
|
||||
? Object.entries(groups).map(
|
||||
([groupName, groupOptions]) => (
|
||||
<CommandGroup
|
||||
key={groupName}
|
||||
heading={groupName}
|
||||
>
|
||||
{groupOptions.map(
|
||||
renderOption
|
||||
)}
|
||||
</CommandGroup>
|
||||
)
|
||||
)
|
||||
: options.map(renderOption)}
|
||||
</CommandList>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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 };
|
||||
@@ -14,6 +14,7 @@ type ToasterToast = ToastProps & {
|
||||
layout?: 'row' | 'column';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const actionTypes = {
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
UPDATE_TOAST: 'UPDATE_TOAST',
|
||||
|
||||
15
src/context/alert-context/alert-context.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import { emptyFn } from '@/lib/utils';
|
||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||
|
||||
export interface AlertContext {
|
||||
showAlert: (params: BaseAlertDialogProps) => void;
|
||||
closeAlert: () => void;
|
||||
}
|
||||
|
||||
export const alertContext = createContext<AlertContext>({
|
||||
closeAlert: emptyFn,
|
||||
showAlert: emptyFn,
|
||||
});
|
||||
|
||||
export const useAlert = () => useContext(alertContext);
|
||||
36
src/context/alert-context/alert-provider.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { AlertContext } from './alert-context';
|
||||
import { alertContext } from './alert-context';
|
||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||
|
||||
export const AlertProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
|
||||
title: '',
|
||||
});
|
||||
const showAlertHandler: AlertContext['showAlert'] = useCallback(
|
||||
(params) => {
|
||||
setAlertParams(params);
|
||||
setShowAlert(true);
|
||||
},
|
||||
[setShowAlert, setAlertParams]
|
||||
);
|
||||
const closeAlertHandler = useCallback(() => {
|
||||
setShowAlert(false);
|
||||
}, [setShowAlert]);
|
||||
|
||||
return (
|
||||
<alertContext.Provider
|
||||
value={{
|
||||
showAlert: showAlertHandler,
|
||||
closeAlert: closeAlertHandler,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
|
||||
</alertContext.Provider>
|
||||
);
|
||||
};
|
||||
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
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,8 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import type { DBSchema } from '@/lib/domain/db-schema';
|
||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||
|
||||
export type ChartDBEventType =
|
||||
| 'add_tables'
|
||||
@@ -70,6 +72,8 @@ export interface ChartDBContext {
|
||||
schemas: DBSchema[];
|
||||
relationships: DBRelationship[];
|
||||
dependencies: DBDependency[];
|
||||
areas: Area[];
|
||||
customTypes: DBCustomType[];
|
||||
currentDiagram: Diagram;
|
||||
events: EventEmitter<ChartDBEvent>;
|
||||
readonly?: boolean;
|
||||
@@ -84,6 +88,7 @@ export interface ChartDBContext {
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
|
||||
loadDiagramFromData: (diagram: Diagram) => void;
|
||||
updateDiagramUpdatedAt: () => Promise<void>;
|
||||
clearDiagramData: () => Promise<void>;
|
||||
deleteDiagram: () => Promise<void>;
|
||||
@@ -220,6 +225,58 @@ export interface ChartDBContext {
|
||||
dependency: Partial<DBDependency>,
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
|
||||
// Area operations
|
||||
createArea: (attributes?: Partial<Omit<Area, 'id'>>) => Promise<Area>;
|
||||
addArea: (
|
||||
area: Area,
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
addAreas: (
|
||||
areas: Area[],
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
getArea: (id: string) => Area | null;
|
||||
removeArea: (
|
||||
id: string,
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
removeAreas: (
|
||||
ids: string[],
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
updateArea: (
|
||||
id: string,
|
||||
area: Partial<Area>,
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
|
||||
// Custom type operations
|
||||
createCustomType: (
|
||||
attributes?: Partial<Omit<DBCustomType, 'id'>>
|
||||
) => Promise<DBCustomType>;
|
||||
addCustomType: (
|
||||
customType: DBCustomType,
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
addCustomTypes: (
|
||||
customTypes: DBCustomType[],
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
getCustomType: (id: string) => DBCustomType | null;
|
||||
removeCustomType: (
|
||||
id: string,
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
removeCustomTypes: (
|
||||
ids: string[],
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
updateCustomType: (
|
||||
id: string,
|
||||
customType: Partial<DBCustomType>,
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export const chartDBContext = createContext<ChartDBContext>({
|
||||
@@ -229,6 +286,8 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
tables: [],
|
||||
relationships: [],
|
||||
dependencies: [],
|
||||
areas: [],
|
||||
customTypes: [],
|
||||
schemas: [],
|
||||
filteredSchemas: [],
|
||||
filterSchemas: emptyFn,
|
||||
@@ -246,6 +305,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
updateDiagramName: emptyFn,
|
||||
updateDiagramUpdatedAt: emptyFn,
|
||||
loadDiagram: emptyFn,
|
||||
loadDiagramFromData: emptyFn,
|
||||
clearDiagramData: emptyFn,
|
||||
deleteDiagram: emptyFn,
|
||||
|
||||
@@ -294,4 +354,22 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
removeDependencies: emptyFn,
|
||||
addDependencies: emptyFn,
|
||||
updateDependency: emptyFn,
|
||||
|
||||
// Area operations
|
||||
createArea: emptyFn,
|
||||
addArea: emptyFn,
|
||||
addAreas: emptyFn,
|
||||
getArea: emptyFn,
|
||||
removeArea: emptyFn,
|
||||
removeAreas: emptyFn,
|
||||
updateArea: emptyFn,
|
||||
|
||||
// Custom type operations
|
||||
createCustomType: emptyFn,
|
||||
addCustomType: emptyFn,
|
||||
addCustomTypes: emptyFn,
|
||||
getCustomType: emptyFn,
|
||||
removeCustomType: emptyFn,
|
||||
removeCustomTypes: emptyFn,
|
||||
updateCustomType: emptyFn,
|
||||
});
|
||||
|
||||
@@ -21,15 +21,24 @@ import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { useEventEmitter } from 'ahooks';
|
||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
import { storageInitialValue } from '../storage-context/storage-context';
|
||||
import { useDiff } from '../diff-context/use-diff';
|
||||
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
||||
import {
|
||||
DBCustomTypeKind,
|
||||
type DBCustomType,
|
||||
} from '@/lib/domain/db-custom-type';
|
||||
|
||||
export interface ChartDBProviderProps {
|
||||
diagram?: Diagram;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export const ChartDBProvider: React.FC<
|
||||
React.PropsWithChildren<ChartDBProviderProps>
|
||||
> = ({ children, diagram, readonly }) => {
|
||||
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||
const { hasDiff } = useDiff();
|
||||
let db = useStorage();
|
||||
const events = useEventEmitter<ChartDBEvent>();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
@@ -52,9 +61,37 @@ export const ChartDBProvider: React.FC<
|
||||
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
||||
diagram?.dependencies ?? []
|
||||
);
|
||||
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
|
||||
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
|
||||
diagram?.customTypes ?? []
|
||||
);
|
||||
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 readonly = useMemo(
|
||||
() => readonlyProp ?? hasDiff ?? false,
|
||||
[readonlyProp, hasDiff]
|
||||
);
|
||||
|
||||
if (readonly) {
|
||||
db = storageInitialValue;
|
||||
}
|
||||
@@ -124,6 +161,8 @@ export const ChartDBProvider: React.FC<
|
||||
tables,
|
||||
relationships,
|
||||
dependencies,
|
||||
areas,
|
||||
customTypes,
|
||||
}),
|
||||
[
|
||||
diagramId,
|
||||
@@ -133,6 +172,8 @@ export const ChartDBProvider: React.FC<
|
||||
tables,
|
||||
relationships,
|
||||
dependencies,
|
||||
areas,
|
||||
customTypes,
|
||||
diagramCreatedAt,
|
||||
diagramUpdatedAt,
|
||||
]
|
||||
@@ -144,6 +185,8 @@ export const ChartDBProvider: React.FC<
|
||||
setTables([]);
|
||||
setRelationships([]);
|
||||
setDependencies([]);
|
||||
setAreas([]);
|
||||
setCustomTypes([]);
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
resetRedoStack();
|
||||
@@ -154,6 +197,8 @@ export const ChartDBProvider: React.FC<
|
||||
db.deleteDiagramTables(diagramId),
|
||||
db.deleteDiagramRelationships(diagramId),
|
||||
db.deleteDiagramDependencies(diagramId),
|
||||
db.deleteDiagramAreas(diagramId),
|
||||
db.deleteDiagramCustomTypes(diagramId),
|
||||
]);
|
||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||
|
||||
@@ -166,6 +211,8 @@ export const ChartDBProvider: React.FC<
|
||||
setTables([]);
|
||||
setRelationships([]);
|
||||
setDependencies([]);
|
||||
setAreas([]);
|
||||
setCustomTypes([]);
|
||||
resetRedoStack();
|
||||
resetUndoStack();
|
||||
|
||||
@@ -174,6 +221,8 @@ export const ChartDBProvider: React.FC<
|
||||
db.deleteDiagramRelationships(diagramId),
|
||||
db.deleteDiagram(diagramId),
|
||||
db.deleteDiagramDependencies(diagramId),
|
||||
db.deleteDiagramAreas(diagramId),
|
||||
db.deleteDiagramCustomTypes(diagramId),
|
||||
]);
|
||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||
|
||||
@@ -255,22 +304,27 @@ export const ChartDBProvider: React.FC<
|
||||
);
|
||||
|
||||
const addTables: ChartDBContext['addTables'] = useCallback(
|
||||
async (tables: DBTable[], options = { updateHistory: true }) => {
|
||||
setTables((currentTables) => [...currentTables, ...tables]);
|
||||
async (tablesToAdd: DBTable[], options = { updateHistory: true }) => {
|
||||
setTables((currentTables) => [...currentTables, ...tablesToAdd]);
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
await Promise.all([
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
...tables.map((table) => db.addTable({ diagramId, table })),
|
||||
...tablesToAdd.map((table) =>
|
||||
db.addTable({ diagramId, table })
|
||||
),
|
||||
]);
|
||||
|
||||
events.emit({ action: 'add_tables', data: { tables } });
|
||||
events.emit({
|
||||
action: 'add_tables',
|
||||
data: { tables: tablesToAdd },
|
||||
});
|
||||
|
||||
if (options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'addTables',
|
||||
redoData: { tables },
|
||||
undoData: { tableIds: tables.map((t) => t.id) },
|
||||
redoData: { tables: tablesToAdd },
|
||||
undoData: { tableIds: tablesToAdd.map((t) => t.id) },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
@@ -310,6 +364,7 @@ export const ChartDBProvider: React.FC<
|
||||
color: randomColor(),
|
||||
createdAt: Date.now(),
|
||||
isView: false,
|
||||
order: tables.length,
|
||||
...attributes,
|
||||
};
|
||||
await addTable(table);
|
||||
@@ -728,13 +783,23 @@ export const ChartDBProvider: React.FC<
|
||||
options = { updateHistory: true }
|
||||
) => {
|
||||
const fields = getTable(tableId)?.fields ?? [];
|
||||
setTables((tables) =>
|
||||
tables.map((table) =>
|
||||
table.id === tableId
|
||||
? { ...table, fields: [...table.fields, field] }
|
||||
: table
|
||||
)
|
||||
);
|
||||
setTables((tables) => {
|
||||
return tables.map((table) => {
|
||||
if (table.id === tableId) {
|
||||
db.updateTable({
|
||||
id: tableId,
|
||||
attributes: {
|
||||
...table,
|
||||
fields: [...table.fields, field],
|
||||
},
|
||||
});
|
||||
|
||||
return { ...table, fields: [...table.fields, field] };
|
||||
}
|
||||
|
||||
return table;
|
||||
});
|
||||
});
|
||||
|
||||
events.emit({
|
||||
action: 'add_field',
|
||||
@@ -755,13 +820,6 @@ export const ChartDBProvider: React.FC<
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
await Promise.all([
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
db.updateTable({
|
||||
id: tableId,
|
||||
attributes: {
|
||||
...table,
|
||||
fields: [...table.fields, field],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (options.updateHistory) {
|
||||
@@ -1334,15 +1392,133 @@ export const ChartDBProvider: React.FC<
|
||||
]
|
||||
);
|
||||
|
||||
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
||||
async (diagramId: string) => {
|
||||
const diagram = await db.getDiagram(diagramId, {
|
||||
includeRelationships: true,
|
||||
includeTables: true,
|
||||
includeDependencies: true,
|
||||
});
|
||||
// Area operations
|
||||
const addAreas: ChartDBContext['addAreas'] = useCallback(
|
||||
async (areas: Area[], options = { updateHistory: true }) => {
|
||||
setAreas((currentAreas) => [...currentAreas, ...areas]);
|
||||
|
||||
if (diagram) {
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
...areas.map((area) => db.addArea({ diagramId, area })),
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
]);
|
||||
|
||||
if (options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'addAreas',
|
||||
redoData: { areas },
|
||||
undoData: { areaIds: areas.map((a) => a.id) },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[db, diagramId, setAreas, addUndoAction, resetRedoStack]
|
||||
);
|
||||
|
||||
const addArea: ChartDBContext['addArea'] = useCallback(
|
||||
async (area: Area, options = { updateHistory: true }) => {
|
||||
return addAreas([area], options);
|
||||
},
|
||||
[addAreas]
|
||||
);
|
||||
|
||||
const createArea: ChartDBContext['createArea'] = useCallback(
|
||||
async (attributes) => {
|
||||
const area: Area = {
|
||||
id: generateId(),
|
||||
name: `Area ${areas.length + 1}`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 300,
|
||||
height: 200,
|
||||
color: randomColor(),
|
||||
...attributes,
|
||||
};
|
||||
|
||||
await addArea(area);
|
||||
|
||||
return area;
|
||||
},
|
||||
[areas, addArea]
|
||||
);
|
||||
|
||||
const getArea: ChartDBContext['getArea'] = useCallback(
|
||||
(id: string) => areas.find((area) => area.id === id) ?? null,
|
||||
[areas]
|
||||
);
|
||||
|
||||
const removeAreas: ChartDBContext['removeAreas'] = useCallback(
|
||||
async (ids: string[], options = { updateHistory: true }) => {
|
||||
const prevAreas = [
|
||||
...areas.filter((area) => ids.includes(area.id)),
|
||||
];
|
||||
|
||||
setAreas((areas) => areas.filter((area) => !ids.includes(area.id)));
|
||||
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
...ids.map((id) => db.deleteArea({ diagramId, id })),
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
]);
|
||||
|
||||
if (prevAreas.length > 0 && options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'removeAreas',
|
||||
redoData: { areaIds: ids },
|
||||
undoData: { areas: prevAreas },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[db, diagramId, setAreas, areas, addUndoAction, resetRedoStack]
|
||||
);
|
||||
|
||||
const removeArea: ChartDBContext['removeArea'] = useCallback(
|
||||
async (id: string, options = { updateHistory: true }) => {
|
||||
return removeAreas([id], options);
|
||||
},
|
||||
[removeAreas]
|
||||
);
|
||||
|
||||
const updateArea: ChartDBContext['updateArea'] = useCallback(
|
||||
async (
|
||||
id: string,
|
||||
area: Partial<Area>,
|
||||
options = { updateHistory: true }
|
||||
) => {
|
||||
const prevArea = getArea(id);
|
||||
|
||||
setAreas((areas) =>
|
||||
areas.map((a) => (a.id === id ? { ...a, ...area } : a))
|
||||
);
|
||||
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
db.updateArea({ id, attributes: area }),
|
||||
]);
|
||||
|
||||
if (!!prevArea && options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'updateArea',
|
||||
redoData: { areaId: id, area },
|
||||
undoData: { areaId: id, area: prevArea },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
|
||||
);
|
||||
|
||||
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
|
||||
useCallback(
|
||||
async (diagram) => {
|
||||
setDiagramId(diagram.id);
|
||||
setDiagramName(diagram.name);
|
||||
setDatabaseType(diagram.databaseType);
|
||||
@@ -1350,26 +1526,189 @@ export const ChartDBProvider: React.FC<
|
||||
setTables(diagram?.tables ?? []);
|
||||
setRelationships(diagram?.relationships ?? []);
|
||||
setDependencies(diagram?.dependencies ?? []);
|
||||
setAreas(diagram?.areas ?? []);
|
||||
setCustomTypes(diagram?.customTypes ?? []);
|
||||
setDiagramCreatedAt(diagram.createdAt);
|
||||
setDiagramUpdatedAt(diagram.updatedAt);
|
||||
|
||||
events.emit({ action: 'load_diagram', data: { diagram } });
|
||||
},
|
||||
[
|
||||
setDiagramId,
|
||||
setDiagramName,
|
||||
setDatabaseType,
|
||||
setDatabaseEdition,
|
||||
setTables,
|
||||
setRelationships,
|
||||
setDependencies,
|
||||
setAreas,
|
||||
setCustomTypes,
|
||||
setDiagramCreatedAt,
|
||||
setDiagramUpdatedAt,
|
||||
events,
|
||||
]
|
||||
);
|
||||
|
||||
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
||||
async (diagramId: string) => {
|
||||
const diagram = await db.getDiagram(diagramId, {
|
||||
includeRelationships: true,
|
||||
includeTables: true,
|
||||
includeDependencies: true,
|
||||
includeAreas: true,
|
||||
includeCustomTypes: true,
|
||||
});
|
||||
|
||||
if (diagram) {
|
||||
loadDiagramFromData(diagram);
|
||||
}
|
||||
|
||||
return diagram;
|
||||
},
|
||||
[db, loadDiagramFromData]
|
||||
);
|
||||
|
||||
// Custom type operations
|
||||
const getCustomType: ChartDBContext['getCustomType'] = useCallback(
|
||||
(id: string) => customTypes.find((type) => type.id === id) ?? null,
|
||||
[customTypes]
|
||||
);
|
||||
|
||||
const addCustomTypes: ChartDBContext['addCustomTypes'] = useCallback(
|
||||
async (
|
||||
customTypes: DBCustomType[],
|
||||
options = { updateHistory: true }
|
||||
) => {
|
||||
setCustomTypes((currentTypes) => [...currentTypes, ...customTypes]);
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
...customTypes.map((customType) =>
|
||||
db.addCustomType({ diagramId, customType })
|
||||
),
|
||||
]);
|
||||
|
||||
if (options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'addCustomTypes',
|
||||
redoData: { customTypes },
|
||||
undoData: { customTypeIds: customTypes.map((t) => t.id) },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[db, diagramId, setCustomTypes, addUndoAction, resetRedoStack]
|
||||
);
|
||||
|
||||
const addCustomType: ChartDBContext['addCustomType'] = useCallback(
|
||||
async (customType: DBCustomType, options = { updateHistory: true }) => {
|
||||
return addCustomTypes([customType], options);
|
||||
},
|
||||
[addCustomTypes]
|
||||
);
|
||||
|
||||
const createCustomType: ChartDBContext['createCustomType'] = useCallback(
|
||||
async (attributes) => {
|
||||
const customType: DBCustomType = {
|
||||
id: generateId(),
|
||||
name: `type_${customTypes.length + 1}`,
|
||||
kind: DBCustomTypeKind.enum,
|
||||
values: [],
|
||||
fields: [],
|
||||
...attributes,
|
||||
};
|
||||
|
||||
await addCustomType(customType);
|
||||
return customType;
|
||||
},
|
||||
[addCustomType, customTypes]
|
||||
);
|
||||
|
||||
const removeCustomTypes: ChartDBContext['removeCustomTypes'] = useCallback(
|
||||
async (ids, options = { updateHistory: true }) => {
|
||||
const typesToRemove = ids
|
||||
.map((id) => getCustomType(id))
|
||||
.filter(Boolean) as DBCustomType[];
|
||||
|
||||
setCustomTypes((types) =>
|
||||
types.filter((type) => !ids.includes(type.id))
|
||||
);
|
||||
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
...ids.map((id) => db.deleteCustomType({ diagramId, id })),
|
||||
]);
|
||||
|
||||
if (typesToRemove.length > 0 && options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'removeCustomTypes',
|
||||
redoData: {
|
||||
customTypeIds: ids,
|
||||
},
|
||||
undoData: {
|
||||
customTypes: typesToRemove,
|
||||
},
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[
|
||||
db,
|
||||
setDiagramId,
|
||||
setDiagramName,
|
||||
setDatabaseType,
|
||||
setDatabaseEdition,
|
||||
setTables,
|
||||
setRelationships,
|
||||
setDependencies,
|
||||
setDiagramCreatedAt,
|
||||
setDiagramUpdatedAt,
|
||||
events,
|
||||
diagramId,
|
||||
setCustomTypes,
|
||||
addUndoAction,
|
||||
resetRedoStack,
|
||||
getCustomType,
|
||||
]
|
||||
);
|
||||
|
||||
const removeCustomType: ChartDBContext['removeCustomType'] = useCallback(
|
||||
async (id: string, options = { updateHistory: true }) => {
|
||||
return removeCustomTypes([id], options);
|
||||
},
|
||||
[removeCustomTypes]
|
||||
);
|
||||
|
||||
const updateCustomType: ChartDBContext['updateCustomType'] = useCallback(
|
||||
async (
|
||||
id: string,
|
||||
customType: Partial<DBCustomType>,
|
||||
options = { updateHistory: true }
|
||||
) => {
|
||||
const prevCustomType = getCustomType(id);
|
||||
setCustomTypes((types) =>
|
||||
types.map((t) => (t.id === id ? { ...t, ...customType } : t))
|
||||
);
|
||||
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
db.updateCustomType({ id, attributes: customType }),
|
||||
]);
|
||||
|
||||
if (!!prevCustomType && options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'updateCustomType',
|
||||
redoData: { customTypeId: id, customType },
|
||||
undoData: { customTypeId: id, customType: prevCustomType },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[
|
||||
db,
|
||||
setCustomTypes,
|
||||
addUndoAction,
|
||||
resetRedoStack,
|
||||
getCustomType,
|
||||
diagramId,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1382,6 +1721,7 @@ export const ChartDBProvider: React.FC<
|
||||
tables,
|
||||
relationships,
|
||||
dependencies,
|
||||
areas,
|
||||
currentDiagram,
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
@@ -1391,6 +1731,7 @@ export const ChartDBProvider: React.FC<
|
||||
updateDiagramId,
|
||||
updateDiagramName,
|
||||
loadDiagram,
|
||||
loadDiagramFromData,
|
||||
updateDatabaseType,
|
||||
updateDatabaseEdition,
|
||||
clearDiagramData,
|
||||
@@ -1428,6 +1769,21 @@ export const ChartDBProvider: React.FC<
|
||||
removeDependency,
|
||||
removeDependencies,
|
||||
updateDependency,
|
||||
createArea,
|
||||
addArea,
|
||||
addAreas,
|
||||
getArea,
|
||||
removeArea,
|
||||
removeAreas,
|
||||
updateArea,
|
||||
customTypes,
|
||||
createCustomType,
|
||||
addCustomType,
|
||||
addCustomTypes,
|
||||
getCustomType,
|
||||
removeCustomType,
|
||||
removeCustomTypes,
|
||||
updateCustomType,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -4,7 +4,10 @@ import type { ChartDBConfig } from '@/lib/domain/config';
|
||||
|
||||
export interface ConfigContext {
|
||||
config?: ChartDBConfig;
|
||||
updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
|
||||
updateConfig: (params: {
|
||||
config?: Partial<ChartDBConfig>;
|
||||
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
export const ConfigContext = createContext<ConfigContext>({
|
||||
|
||||
@@ -19,15 +19,29 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
loadConfig();
|
||||
}, [getConfig]);
|
||||
|
||||
const updateConfig: ConfigContext['updateConfig'] = async (
|
||||
config: Partial<ChartDBConfig>
|
||||
) => {
|
||||
await updateDataConfig(config);
|
||||
setConfig((prevConfig) =>
|
||||
prevConfig
|
||||
? { ...prevConfig, ...config }
|
||||
: { ...{ defaultDiagramId: '' }, ...config }
|
||||
);
|
||||
const updateConfig: ConfigContext['updateConfig'] = async ({
|
||||
config,
|
||||
updateFn,
|
||||
}) => {
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
setConfig((prevConfig) => {
|
||||
let baseConfig: ChartDBConfig = { defaultDiagramId: '' };
|
||||
if (prevConfig) {
|
||||
baseConfig = prevConfig;
|
||||
}
|
||||
|
||||
const updatedConfig = updateFn
|
||||
? updateFn(baseConfig)
|
||||
: { ...baseConfig, ...config };
|
||||
|
||||
updateDataConfig(updatedConfig).then(() => {
|
||||
resolve();
|
||||
});
|
||||
return updatedConfig;
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
import { createContext } from 'react';
|
||||
import { emptyFn } from '@/lib/utils';
|
||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||
import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table-schema-dialog';
|
||||
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-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 { 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';
|
||||
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
|
||||
|
||||
export interface DialogContext {
|
||||
// Create diagram dialog
|
||||
openCreateDiagramDialog: () => void;
|
||||
openCreateDiagramDialog: (
|
||||
params?: Omit<CreateDiagramDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeCreateDiagramDialog: () => void;
|
||||
|
||||
// Open diagram dialog
|
||||
openOpenDiagramDialog: () => void;
|
||||
openOpenDiagramDialog: (
|
||||
params?: Omit<OpenDiagramDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeOpenDiagramDialog: () => void;
|
||||
|
||||
// Export SQL dialog
|
||||
openExportSQLDialog: (params: Omit<ExportSQLDialogProps, 'dialog'>) => void;
|
||||
closeExportSQLDialog: () => void;
|
||||
|
||||
// Alert dialog
|
||||
showAlert: (params: BaseAlertDialogProps) => void;
|
||||
closeAlert: () => void;
|
||||
|
||||
// Create relationship dialog
|
||||
openCreateRelationshipDialog: () => void;
|
||||
openCreateRelationshipDialog: (
|
||||
params?: Omit<CreateRelationshipDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeCreateRelationshipDialog: () => void;
|
||||
|
||||
// Import database dialog
|
||||
@@ -62,6 +67,12 @@ export interface DialogContext {
|
||||
params: Omit<ImportDiagramDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeImportDiagramDialog: () => void;
|
||||
|
||||
// Import DBML dialog
|
||||
openImportDBMLDialog: (
|
||||
params?: Omit<ImportDBMLDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeImportDBMLDialog: () => void;
|
||||
}
|
||||
|
||||
export const dialogContext = createContext<DialogContext>({
|
||||
@@ -71,8 +82,6 @@ export const dialogContext = createContext<DialogContext>({
|
||||
closeOpenDiagramDialog: emptyFn,
|
||||
openExportSQLDialog: emptyFn,
|
||||
closeExportSQLDialog: emptyFn,
|
||||
closeAlert: emptyFn,
|
||||
showAlert: emptyFn,
|
||||
closeCreateRelationshipDialog: emptyFn,
|
||||
openCreateRelationshipDialog: emptyFn,
|
||||
openImportDatabaseDialog: emptyFn,
|
||||
@@ -87,4 +96,6 @@ export const dialogContext = createContext<DialogContext>({
|
||||
closeExportDiagramDialog: emptyFn,
|
||||
openImportDiagramDialog: emptyFn,
|
||||
closeImportDiagramDialog: emptyFn,
|
||||
openImportDBMLDialog: emptyFn,
|
||||
closeImportDBMLDialog: emptyFn,
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { DialogContext } from './dialog-context';
|
||||
import { dialogContext } from './dialog-context';
|
||||
import type { CreateDiagramDialogProps } 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 type { ExportSQLDialogProps } 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 type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||
import type { CreateRelationshipDialogProps } 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 { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||
@@ -19,15 +20,50 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
|
||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-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> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
|
||||
const [newDiagramDialogParams, setNewDiagramDialogParams] =
|
||||
useState<Omit<CreateDiagramDialogProps, 'dialog'>>();
|
||||
const openNewDiagramDialogHandler: DialogContext['openCreateDiagramDialog'] =
|
||||
useCallback(
|
||||
(props) => {
|
||||
setNewDiagramDialogParams(props);
|
||||
setOpenNewDiagramDialog(true);
|
||||
},
|
||||
[setOpenNewDiagramDialog]
|
||||
);
|
||||
|
||||
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] =
|
||||
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);
|
||||
|
||||
// Export image dialog
|
||||
@@ -88,7 +124,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[setOpenTableSchemaDialog]
|
||||
);
|
||||
|
||||
// Export image dialog
|
||||
// Export diagram dialog
|
||||
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
|
||||
useState(false);
|
||||
|
||||
@@ -96,35 +132,22 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
|
||||
useState(false);
|
||||
|
||||
// Alert dialog
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
|
||||
title: '',
|
||||
});
|
||||
const showAlertHandler: DialogContext['showAlert'] = useCallback(
|
||||
(params) => {
|
||||
setAlertParams(params);
|
||||
setShowAlert(true);
|
||||
},
|
||||
[setShowAlert, setAlertParams]
|
||||
);
|
||||
const closeAlertHandler = useCallback(() => {
|
||||
setShowAlert(false);
|
||||
}, [setShowAlert]);
|
||||
// Import DBML dialog
|
||||
const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
|
||||
const [importDBMLDialogParams, setImportDBMLDialogParams] =
|
||||
useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
|
||||
|
||||
return (
|
||||
<dialogContext.Provider
|
||||
value={{
|
||||
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
|
||||
openCreateDiagramDialog: openNewDiagramDialogHandler,
|
||||
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
|
||||
openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true),
|
||||
openOpenDiagramDialog: openOpenDiagramDialogHandler,
|
||||
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
||||
openExportSQLDialog: openExportSQLDialogHandler,
|
||||
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
|
||||
showAlert: showAlertHandler,
|
||||
closeAlert: closeAlertHandler,
|
||||
openCreateRelationshipDialog: () =>
|
||||
setOpenCreateRelationshipDialog(true),
|
||||
openCreateRelationshipDialog:
|
||||
openCreateRelationshipDialogHandler,
|
||||
closeCreateRelationshipDialog: () =>
|
||||
setOpenCreateRelationshipDialog(false),
|
||||
openImportDatabaseDialog: openImportDatabaseDialogHandler,
|
||||
@@ -142,18 +165,29 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
|
||||
closeImportDiagramDialog: () =>
|
||||
setOpenImportDiagramDialog(false),
|
||||
openImportDBMLDialog: (params) => {
|
||||
setImportDBMLDialogParams(params);
|
||||
setOpenImportDBMLDialog(true);
|
||||
},
|
||||
closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
|
||||
<OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} />
|
||||
<CreateDiagramDialog
|
||||
dialog={{ open: openNewDiagramDialog }}
|
||||
{...newDiagramDialogParams}
|
||||
/>
|
||||
<OpenDiagramDialog
|
||||
dialog={{ open: openOpenDiagramDialog }}
|
||||
{...openDiagramDialogParams}
|
||||
/>
|
||||
<ExportSQLDialog
|
||||
dialog={{ open: openExportSQLDialog }}
|
||||
{...exportSQLDialogParams}
|
||||
/>
|
||||
<BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
|
||||
<CreateRelationshipDialog
|
||||
dialog={{ open: openCreateRelationshipDialog }}
|
||||
{...createRelationshipDialogParams}
|
||||
/>
|
||||
<ImportDatabaseDialog
|
||||
dialog={{ open: openImportDatabaseDialog }}
|
||||
@@ -170,6 +204,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
/>
|
||||
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
||||
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
||||
<ImportDBMLDialog
|
||||
dialog={{ open: openImportDBMLDialog }}
|
||||
{...importDBMLDialogParams}
|
||||
/>
|
||||
</dialogContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||