Compare commits
26 Commits
cli@1.35.0
...
cli@1.34.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d15717b67a | ||
|
|
a3c7f89eda | ||
|
|
6692e0dfc0 | ||
|
|
8541165781 | ||
|
|
5cd5ebbc65 | ||
|
|
5066ef708a | ||
|
|
a6a378c5a6 | ||
|
|
a3a3cf205d | ||
|
|
3fd2e63db3 | ||
|
|
f5956f1b2e | ||
|
|
f3b397b0d8 | ||
|
|
b7940087ee | ||
|
|
3dae655858 | ||
|
|
2aa269734b | ||
|
|
bc91836f83 | ||
|
|
6d8b243571 | ||
|
|
c9967b1a6d | ||
|
|
7f72aadff9 | ||
|
|
8faf9565bb | ||
|
|
7ac3f12852 | ||
|
|
184a3ed190 | ||
|
|
372c4e32d4 | ||
|
|
a68d261d8e | ||
|
|
55bda3f56b | ||
|
|
2311e1dd77 | ||
|
|
824ee142c4 |
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -32,6 +32,7 @@ Where `PKG` is:
|
|||||||
- `deps`: For changes to dependencies
|
- `deps`: For changes to dependencies
|
||||||
- `docs`: For changes to the documentation
|
- `docs`: For changes to the documentation
|
||||||
- `examples`: For changes to the examples
|
- `examples`: For changes to the examples
|
||||||
|
- `internal/lib`: For changes to Nhost's common libraries (internal)
|
||||||
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
|
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
|
||||||
- `nhost-js`: For changes to the Nhost JavaScript SDK
|
- `nhost-js`: For changes to the Nhost JavaScript SDK
|
||||||
- `nixops`: For changes to the NixOps
|
- `nixops`: For changes to the NixOps
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ runs:
|
|||||||
|
|
||||||
# Define valid types and packages
|
# Define valid types and packages
|
||||||
VALID_TYPES="feat|fix|chore"
|
VALID_TYPES="feat|fix|chore"
|
||||||
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage"
|
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|internal\/lib|mintlify-openapi|nhost-js|nixops|storage"
|
||||||
|
|
||||||
# Check if title matches the pattern TYPE(PKG): SUMMARY
|
# Check if title matches the pattern TYPE(PKG): SUMMARY
|
||||||
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then
|
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then
|
||||||
|
|||||||
1
.github/workflows/auth_checks.yaml
vendored
1
.github/workflows/auth_checks.yaml
vendored
@@ -17,6 +17,7 @@ on:
|
|||||||
- '.golangci.yaml'
|
- '.golangci.yaml'
|
||||||
- 'go.mod'
|
- 'go.mod'
|
||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
|
- 'internal/lib/**'
|
||||||
- 'vendor/**'
|
- 'vendor/**'
|
||||||
|
|
||||||
# auth
|
# auth
|
||||||
|
|||||||
2
.github/workflows/ci_update_changelog.yaml
vendored
2
.github/workflows/ci_update_changelog.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
cd ${{ matrix.project }}
|
cd ${{ matrix.project }}
|
||||||
TAG_NAME=$(make release-tag-name)
|
TAG_NAME=$(make release-tag-name)
|
||||||
VERSION=$(nix develop .\#cliff -c make changelog-next-version)
|
VERSION=$(nix develop .\#cliff -c make changelog-next-version)
|
||||||
if git tag | grep -q "$TAG_NAME@$VERSION"; then
|
if git tag | grep -qx "$TAG_NAME@$VERSION"; then
|
||||||
echo "Tag $TAG_NAME@$VERSION already exists, skipping release preparation"
|
echo "Tag $TAG_NAME@$VERSION already exists, skipping release preparation"
|
||||||
else
|
else
|
||||||
echo "Tag $TAG_NAME@$VERSION does not exist, proceeding with release preparation"
|
echo "Tag $TAG_NAME@$VERSION does not exist, proceeding with release preparation"
|
||||||
|
|||||||
2
.github/workflows/dashboard_wf_release.yaml
vendored
2
.github/workflows/dashboard_wf_release.yaml
vendored
@@ -88,7 +88,7 @@ jobs:
|
|||||||
- name: Bump version in source code
|
- name: Bump version in source code
|
||||||
run: |
|
run: |
|
||||||
find cli -type f -exec sed -i 's/"nhost\/dashboard:[^"]*"/"nhost\/dashboard:${{ inputs.VERSION }}"/g' {} +
|
find cli -type f -exec sed -i 's/"nhost\/dashboard:[^"]*"/"nhost\/dashboard:${{ inputs.VERSION }}"/g' {} +
|
||||||
sed -i 's/"nhost\/dashboard:[^"]*"/"nhost\/dashboard:${{ inputs.VERSION }}"/g' docs/reference/cli/commands.mdx
|
sed -i 's/nhost\/dashboard:[^)]*/nhost\/dashboard:${{ inputs.VERSION }}/g' docs/reference/cli/commands.mdx
|
||||||
|
|
||||||
- name: "Create Pull Request"
|
- name: "Create Pull Request"
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v7
|
||||||
|
|||||||
2
.github/workflows/gen_ai_review.yaml
vendored
2
.github/workflows/gen_ai_review.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: PR Agent action step
|
- name: PR Agent action step
|
||||||
id: pragent
|
id: pragent
|
||||||
uses: Codium-ai/pr-agent@v0.30
|
uses: Codium-ai/pr-agent@v0.31
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
|||||||
1
.github/workflows/storage_checks.yaml
vendored
1
.github/workflows/storage_checks.yaml
vendored
@@ -17,6 +17,7 @@ on:
|
|||||||
- '.golangci.yaml'
|
- '.golangci.yaml'
|
||||||
- 'go.mod'
|
- 'go.mod'
|
||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
|
- 'internal/lib/**'
|
||||||
- 'vendor/**'
|
- 'vendor/**'
|
||||||
|
|
||||||
# storage
|
# storage
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
## [cli@1.34.6] - 2025-11-13
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(cli)* Mcp: specify items type for arrays in tools (#3687)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(cli)* Update bindings (#3689)
|
||||||
|
|
||||||
|
## [cli@1.34.5] - 2025-11-06
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(nixops)* Bump go to 1.25.3 and nixpkgs due to CVEs (#3652)
|
||||||
|
- *(cli)* Udpate certs and schema (#3675)
|
||||||
|
- *(cli)* Bump nhost/dashboard to 2.41.0 (#3669)
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func CommandCloud() *cli.Command {
|
|||||||
&cli.StringFlag{ //nolint:exhaustruct
|
&cli.StringFlag{ //nolint:exhaustruct
|
||||||
Name: flagDashboardVersion,
|
Name: flagDashboardVersion,
|
||||||
Usage: "Dashboard version to use",
|
Usage: "Dashboard version to use",
|
||||||
Value: "nhost/dashboard:2.40.0",
|
Value: "nhost/dashboard:2.41.0",
|
||||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||||
},
|
},
|
||||||
&cli.StringFlag{ //nolint:exhaustruct
|
&cli.StringFlag{ //nolint:exhaustruct
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func CommandUp() *cli.Command { //nolint:funlen
|
|||||||
&cli.StringFlag{ //nolint:exhaustruct
|
&cli.StringFlag{ //nolint:exhaustruct
|
||||||
Name: flagDashboardVersion,
|
Name: flagDashboardVersion,
|
||||||
Usage: "Dashboard version to use",
|
Usage: "Dashboard version to use",
|
||||||
Value: "nhost/dashboard:2.40.0",
|
Value: "nhost/dashboard:2.41.0",
|
||||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||||
},
|
},
|
||||||
&cli.StringFlag{ //nolint:exhaustruct
|
&cli.StringFlag{ //nolint:exhaustruct
|
||||||
|
|||||||
@@ -196,10 +196,12 @@ config validate after making changes to your nhost.toml file to ensure it is val
|
|||||||
"mutations": map[string]any{
|
"mutations": map[string]any{
|
||||||
"description": string("list of mutations to fetch"),
|
"description": string("list of mutations to fetch"),
|
||||||
"type": string("array"),
|
"type": string("array"),
|
||||||
|
"items": map[string]any{"type": string("string")},
|
||||||
},
|
},
|
||||||
"queries": map[string]any{
|
"queries": map[string]any{
|
||||||
"description": string("list of queries to fetch"),
|
"description": string("list of queries to fetch"),
|
||||||
"type": string("array"),
|
"type": string("array"),
|
||||||
|
"items": map[string]any{"type": string("string")},
|
||||||
},
|
},
|
||||||
"summary": map[string]any{
|
"summary": map[string]any{
|
||||||
"default": bool(true),
|
"default": bool(true),
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func expectedAuth() *Service {
|
|||||||
"AUTH_PROVIDER_APPLE_ENABLED": "true",
|
"AUTH_PROVIDER_APPLE_ENABLED": "true",
|
||||||
"AUTH_PROVIDER_APPLE_KEY_ID": "appleKeyId",
|
"AUTH_PROVIDER_APPLE_KEY_ID": "appleKeyId",
|
||||||
"AUTH_PROVIDER_APPLE_PRIVATE_KEY": "applePrivateKey",
|
"AUTH_PROVIDER_APPLE_PRIVATE_KEY": "applePrivateKey",
|
||||||
|
"AUTH_PROVIDER_APPLE_SCOPE": "",
|
||||||
"AUTH_PROVIDER_APPLE_TEAM_ID": "appleTeamId",
|
"AUTH_PROVIDER_APPLE_TEAM_ID": "appleTeamId",
|
||||||
"AUTH_PROVIDER_AZUREAD_CLIENT_ID": "azureadClientId",
|
"AUTH_PROVIDER_AZUREAD_CLIENT_ID": "azureadClientId",
|
||||||
"AUTH_PROVIDER_AZUREAD_CLIENT_SECRET": "azureadClientSecret",
|
"AUTH_PROVIDER_AZUREAD_CLIENT_SECRET": "azureadClientSecret",
|
||||||
@@ -75,9 +76,12 @@ func expectedAuth() *Service {
|
|||||||
"AUTH_PROVIDER_FACEBOOK_CLIENT_SECRET": "facebookClientSecret",
|
"AUTH_PROVIDER_FACEBOOK_CLIENT_SECRET": "facebookClientSecret",
|
||||||
"AUTH_PROVIDER_FACEBOOK_ENABLED": "true",
|
"AUTH_PROVIDER_FACEBOOK_ENABLED": "true",
|
||||||
"AUTH_PROVIDER_FACEBOOK_SCOPE": "email",
|
"AUTH_PROVIDER_FACEBOOK_SCOPE": "email",
|
||||||
|
"AUTH_PROVIDER_GITHUB_AUDIENCE": "audience",
|
||||||
"AUTH_PROVIDER_GITHUB_CLIENT_ID": "githubClientId",
|
"AUTH_PROVIDER_GITHUB_CLIENT_ID": "githubClientId",
|
||||||
"AUTH_PROVIDER_GITHUB_CLIENT_SECRET": "githubClientSecret",
|
"AUTH_PROVIDER_GITHUB_CLIENT_SECRET": "githubClientSecret",
|
||||||
"AUTH_PROVIDER_GITHUB_ENABLED": "true",
|
"AUTH_PROVIDER_GITHUB_ENABLED": "true",
|
||||||
|
"AUTH_PROVIDER_GITHUB_SCOPE": "user:email",
|
||||||
|
"AUTH_PROVIDER_GITLAB_AUDIENCE": "audience",
|
||||||
"AUTH_PROVIDER_GITLAB_CLIENT_ID": "gitlabClientId",
|
"AUTH_PROVIDER_GITLAB_CLIENT_ID": "gitlabClientId",
|
||||||
"AUTH_PROVIDER_GITLAB_CLIENT_SECRET": "gitlabClientSecret",
|
"AUTH_PROVIDER_GITLAB_CLIENT_SECRET": "gitlabClientSecret",
|
||||||
"AUTH_PROVIDER_GITLAB_ENABLED": "true",
|
"AUTH_PROVIDER_GITLAB_ENABLED": "true",
|
||||||
@@ -97,6 +101,7 @@ func expectedAuth() *Service {
|
|||||||
"AUTH_PROVIDER_SPOTIFY_CLIENT_SECRET": "spotifyClientSecret",
|
"AUTH_PROVIDER_SPOTIFY_CLIENT_SECRET": "spotifyClientSecret",
|
||||||
"AUTH_PROVIDER_SPOTIFY_ENABLED": "true",
|
"AUTH_PROVIDER_SPOTIFY_ENABLED": "true",
|
||||||
"AUTH_PROVIDER_SPOTIFY_SCOPE": "user-read-email",
|
"AUTH_PROVIDER_SPOTIFY_SCOPE": "user-read-email",
|
||||||
|
"AUTH_PROVIDER_STRAVA_AUDIENCE": "audience",
|
||||||
"AUTH_PROVIDER_STRAVA_CLIENT_ID": "stravaClientId",
|
"AUTH_PROVIDER_STRAVA_CLIENT_ID": "stravaClientId",
|
||||||
"AUTH_PROVIDER_STRAVA_CLIENT_SECRET": "stravaClientSecret",
|
"AUTH_PROVIDER_STRAVA_CLIENT_SECRET": "stravaClientSecret",
|
||||||
"AUTH_PROVIDER_STRAVA_ENABLED": "true",
|
"AUTH_PROVIDER_STRAVA_ENABLED": "true",
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ import (
|
|||||||
// Releases:
|
// Releases:
|
||||||
//
|
//
|
||||||
// https://github.com/nhost/hasura-storage/releases
|
// https://github.com/nhost/hasura-storage/releases
|
||||||
version: string | *"0.8.2"
|
version: string | *"0.9.1"
|
||||||
|
|
||||||
// Networking (custom domains at the moment) are not allowed as we need to do further
|
// Networking (custom domains at the moment) are not allowed as we need to do further
|
||||||
// configurations in the CDN. We will enable it again in the future.
|
// configurations in the CDN. We will enable it again in the future.
|
||||||
@@ -311,7 +311,7 @@ import (
|
|||||||
// Releases:
|
// Releases:
|
||||||
//
|
//
|
||||||
// https://github.com/nhost/hasura-auth/releases
|
// https://github.com/nhost/hasura-auth/releases
|
||||||
version: string | *"0.42.4"
|
version: string | *"0.43.0"
|
||||||
|
|
||||||
// Resources for the service
|
// Resources for the service
|
||||||
resources?: #Resources
|
resources?: #Resources
|
||||||
|
|||||||
@@ -68,10 +68,12 @@ func (t *Tool) Register(mcpServer *server.MCPServer) {
|
|||||||
),
|
),
|
||||||
mcp.WithArray(
|
mcp.WithArray(
|
||||||
"queries",
|
"queries",
|
||||||
|
mcp.WithStringItems(),
|
||||||
mcp.Description("list of queries to fetch"),
|
mcp.Description("list of queries to fetch"),
|
||||||
),
|
),
|
||||||
mcp.WithArray(
|
mcp.WithArray(
|
||||||
"mutations",
|
"mutations",
|
||||||
|
mcp.WithStringItems(),
|
||||||
mcp.Description("list of mutations to fetch"),
|
mcp.Description("list of mutations to fetch"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2247,6 +2247,14 @@ type AuthUserProvidersMinOrderBy struct {
|
|||||||
ProviderID *OrderBy `json:"providerId,omitempty"`
|
ProviderID *OrderBy `json:"providerId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// response of any mutation on the table "auth.user_providers"
|
||||||
|
type AuthUserProvidersMutationResponse struct {
|
||||||
|
// number of rows affected by the mutation
|
||||||
|
AffectedRows int64 `json:"affected_rows"`
|
||||||
|
// data from the rows affected by the mutation
|
||||||
|
Returning []*AuthUserProviders `json:"returning"`
|
||||||
|
}
|
||||||
|
|
||||||
// Ordering options when selecting data from "auth.user_providers".
|
// Ordering options when selecting data from "auth.user_providers".
|
||||||
type AuthUserProvidersOrderBy struct {
|
type AuthUserProvidersOrderBy struct {
|
||||||
ID *OrderBy `json:"id,omitempty"`
|
ID *OrderBy `json:"id,omitempty"`
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIERDCCA8mgAwIBAgISBmRex3kpZ4Mz1/1kq05iqja/MAoGCCqGSM49BAMDMDIx
|
MIIERTCCA8ugAwIBAgISBWD/E+b14mP5jv4DGWRVYv8fMAoGCCqGSM49BAMDMDIx
|
||||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
||||||
ODAeFw0yNTEwMDIxMDUxNDBaFw0yNTEyMzExMDUxMzlaMB8xHTAbBgNVBAMTFGxv
|
ODAeFw0yNTExMDYxMDUxMTBaFw0yNjAyMDQxMDUxMDlaMB8xHTAbBgNVBAMTFGxv
|
||||||
Y2FsLmF1dGgubmhvc3QucnVuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2cVM
|
Y2FsLmF1dGgubmhvc3QucnVuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOah5
|
||||||
ojf8iXZGLneNfnke5LMJIxyTEeGbNOfCv4SOR4K/N4OkpvkUVbH2bRvX99uE9jaK
|
ZLuUQp3pdMBxBWnT6E6/amW9LerKKEEdy3Nc8iAwG9LlnPH0z3m7a9wgEhpFEdlL
|
||||||
515Y48PzPA/4+W1zTKOCAtAwggLMMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAU
|
Rr+qO+NhSRnv6+UF5KOCAtIwggLOMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAU
|
||||||
BggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUQqan
|
BggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUGyb1
|
||||||
raZoU5klAxsgkEVEMIkxmMQwHwYDVR0jBBgwFoAUjw0TovYuftFQbDMYOF1ZjiNy
|
TVK/0vf3uHO4x3R094aG2rEwHwYDVR0jBBgwFoAUjw0TovYuftFQbDMYOF1ZjiNy
|
||||||
kcowMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAChhZodHRwOi8vZTguaS5sZW5j
|
kcowMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAChhZodHRwOi8vZTguaS5sZW5j
|
||||||
ci5vcmcvMIHOBgNVHREEgcYwgcOCFGxvY2FsLmF1dGgubmhvc3QucnVughlsb2Nh
|
ci5vcmcvMIHOBgNVHREEgcYwgcOCFGxvY2FsLmF1dGgubmhvc3QucnVughlsb2Nh
|
||||||
bC5kYXNoYm9hcmQubmhvc3QucnVughJsb2NhbC5kYi5uaG9zdC5ydW6CGWxvY2Fs
|
bC5kYXNoYm9hcmQubmhvc3QucnVughJsb2NhbC5kYi5uaG9zdC5ydW6CGWxvY2Fs
|
||||||
LmZ1bmN0aW9ucy5uaG9zdC5ydW6CF2xvY2FsLmdyYXBocWwubmhvc3QucnVughZs
|
LmZ1bmN0aW9ucy5uaG9zdC5ydW6CF2xvY2FsLmdyYXBocWwubmhvc3QucnVughZs
|
||||||
b2NhbC5oYXN1cmEubmhvc3QucnVughdsb2NhbC5tYWlsaG9nLm5ob3N0LnJ1boIX
|
b2NhbC5oYXN1cmEubmhvc3QucnVughdsb2NhbC5tYWlsaG9nLm5ob3N0LnJ1boIX
|
||||||
bG9jYWwuc3RvcmFnZS5uaG9zdC5ydW4wEwYDVR0gBAwwCjAIBgZngQwBAgEwLQYD
|
bG9jYWwuc3RvcmFnZS5uaG9zdC5ydW4wEwYDVR0gBAwwCjAIBgZngQwBAgEwLQYD
|
||||||
VR0fBCYwJDAioCCgHoYcaHR0cDovL2U4LmMubGVuY3Iub3JnLzY0LmNybDCCAQIG
|
VR0fBCYwJDAioCCgHoYcaHR0cDovL2U4LmMubGVuY3Iub3JnLzMyLmNybDCCAQQG
|
||||||
CisGAQQB1nkCBAIEgfMEgfAA7gB1AO08S9boBsKkogBX28sk4jgB31Ev7cSGxXAP
|
CisGAQQB1nkCBAIEgfUEgfIA8AB2ABmG1Mcoqm/+ugNveCpNAZGqzi1yMQ+uzl1w
|
||||||
IN23Pj/gAAABmaTCI4YAAAQDAEYwRAIgXLRFL1EAXfvN6kd5m6udqlxfz4+5B6rq
|
QS0lTMfUAAABmlkAQokAAAQDAEcwRQIgWDtSxJfM2xcjvScVHOkn8bipzBhNhTnm
|
||||||
Cdhp/ZwDAZ8CIFYvalTkl5NEBEMD3vpPvrj8s1Yy2xsropEh/AvpavvLAHUAGYbU
|
B89TDh1/4XUCIQDe08W33PCx2D+akCdW9U9mZKQpIW6deLZSI3ZWpSNKMAB2AA5X
|
||||||
xyiqb/66A294Kk0BkarOLXIxD67OXXBBLSVMx9QAAAGZpMIjhwAABAMARjBEAiBk
|
lLzzrqk+MxssmQez95Dfm8I9cTIl3SGpJaxhxU4hAAABmlkAQn8AAAQDAEcwRQIg
|
||||||
H1vqU9HNuBcf4UYL/xZ42BeUAARHStiFaIZtnR1kEgIgbIJ0CGqIpxmWuwCunl9p
|
KnojmNTpNk1OFTQI0EnlPa2bpwqmUgmUCLeqE6SWfgoCIQCrhZbxYPHbGLF/HpRq
|
||||||
ar+rGLdQrCk9BZXq/VjPPAAwCgYIKoZIzj0EAwMDaQAwZgIxAKvk5a2zQsv7JLNj
|
vCTcOh24SRCuxlkqtaowbbfmKjAKBggqhkjOPQQDAwNoADBlAjEArstFIC+KAsfQ
|
||||||
NO1ly+DI8qiy5nf4HQrOrHOjtmx5RUu0HSO9P0J0u069qAqXMgIxAMLdME9JUo2c
|
nLhtqsaNzkhftN5adDyr2CoE0WUPF1sLDi+xDnDO+JgIPL0YKAFNAjATJ4omhpc+
|
||||||
TJo3pwWv5MRyg/MkOJ4ImKdDJXfIZNkEIUyP3vwTqImvZe07gJDsYg==
|
I6/kWcef2RyO9YCGQQE9pdez5CYKb9o8YAntDSHM3b5nXXj3AX/USdQ=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
|
MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgfJZOkvawA0vBMw9W
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgInXN4JRnXNTjx7rM
|
||||||
ph8i1Z+SJQrFscPbqSYpxngzEDahRANCAATZxUyiN/yJdkYud41+eR7kswkjHJMR
|
avurZrN1EV1iebQeNUlMlFp7VJ+hRANCAAQ5qHlku5RCnel0wHEFadPoTr9qZb0t
|
||||||
4Zs058K/hI5Hgr83g6Sm+RRVsfZtG9f324T2NornXljjw/M8D/j5bXNM
|
6sooQR3Lc1zyIDAb0uWc8fTPebtr3CASGkUR2UtGv6o742FJGe/r5QXk
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIEWDCCA96gAwIBAgISBbvrSsjDQm4zevwwjxFGmeTMMAoGCCqGSM49BAMDMDIx
|
MIIEVzCCA92gAwIBAgISBm54VdkoqD8s8efq7ceHaTihMAoGCCqGSM49BAMDMDIx
|
||||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
||||||
NzAeFw0yNTEwMDIxMDUyNTdaFw0yNTEyMzExMDUyNTZaMCExHzAdBgNVBAMMFiou
|
ODAeFw0yNTExMDYxMDUyMjBaFw0yNjAyMDQxMDUyMTlaMCExHzAdBgNVBAMMFiou
|
||||||
YXV0aC5sb2NhbC5uaG9zdC5ydW4wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATG
|
YXV0aC5sb2NhbC5uaG9zdC5ydW4wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASI
|
||||||
x0o7t0pSrOoFc+pljtqJVxgaSW+w9D9C2WdysMeSKKOU+0MzaM4ynLUhETOpBs8E
|
rTkZOM4ip42DCyDADXGc7oV3+OkimyTM3st2RIZWG28rFRwH0LebJV2cduq1Hdtl
|
||||||
612mdcoeak+G1Emj6UVwo4IC4zCCAt8wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQW
|
VxIEr+RhvyIL7gllueXUo4IC4jCCAt4wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQW
|
||||||
MBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQ+
|
MBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTw
|
||||||
lVsLiXSRLAECs9OgkCEBS7jMmzAfBgNVHSMEGDAWgBSuSJ7chx1EoG/aouVgdAR4
|
bM86O381+aljU3oTUvwhZ90PCDAfBgNVHSMEGDAWgBSPDROi9i5+0VBsMxg4XVmO
|
||||||
wpwAgDAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly9lNy5pLmxl
|
I3KRyjAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly9lOC5pLmxl
|
||||||
bmNyLm9yZy8wgd4GA1UdEQSB1jCB04IWKi5hdXRoLmxvY2FsLm5ob3N0LnJ1boIb
|
bmNyLm9yZy8wgd4GA1UdEQSB1jCB04IWKi5hdXRoLmxvY2FsLm5ob3N0LnJ1boIb
|
||||||
Ki5kYXNoYm9hcmQubG9jYWwubmhvc3QucnVughQqLmRiLmxvY2FsLm5ob3N0LnJ1
|
Ki5kYXNoYm9hcmQubG9jYWwubmhvc3QucnVughQqLmRiLmxvY2FsLm5ob3N0LnJ1
|
||||||
boIbKi5mdW5jdGlvbnMubG9jYWwubmhvc3QucnVughkqLmdyYXBocWwubG9jYWwu
|
boIbKi5mdW5jdGlvbnMubG9jYWwubmhvc3QucnVughkqLmdyYXBocWwubG9jYWwu
|
||||||
bmhvc3QucnVughgqLmhhc3VyYS5sb2NhbC5uaG9zdC5ydW6CGSoubWFpbGhvZy5s
|
bmhvc3QucnVughgqLmhhc3VyYS5sb2NhbC5uaG9zdC5ydW6CGSoubWFpbGhvZy5s
|
||||||
b2NhbC5uaG9zdC5ydW6CGSouc3RvcmFnZS5sb2NhbC5uaG9zdC5ydW4wEwYDVR0g
|
b2NhbC5uaG9zdC5ydW6CGSouc3RvcmFnZS5sb2NhbC5uaG9zdC5ydW4wEwYDVR0g
|
||||||
BAwwCjAIBgZngQwBAgEwLQYDVR0fBCYwJDAioCCgHoYcaHR0cDovL2U3LmMubGVu
|
BAwwCjAIBgZngQwBAgEwLQYDVR0fBCYwJDAioCCgHoYcaHR0cDovL2U4LmMubGVu
|
||||||
Y3Iub3JnLzc3LmNybDCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB2AN3cyjSV1+EW
|
Y3Iub3JnLzM0LmNybDCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AEmcm2neHXzs
|
||||||
BeeVMvrHn/g9HFDf2wA6FBJ2Ciysu8gqAAABmaTDUHkAAAQDAEcwRQIgWudJ8XKA
|
/DbezYdkprhbrwqHgBnRVVL76esp3fjDAAABmlkBVgkAAAQDAEcwRQIhANH6Ml3u
|
||||||
BT5jq5Tl0xQLNb953pBi22Tb0TIWk+RSqHgCIQDsTrLVMFaQTV7EFCY1tFhi5qae
|
IM4nAzwAIjIjBjn8EWbn1ZHfgwO+rlSo5rzpAiATPKE8Mx5LK1IayG5VCK1eCDyc
|
||||||
SCpEwwdFcnom/nz6EAB3AO08S9boBsKkogBX28sk4jgB31Ev7cSGxXAPIN23Pj/g
|
rzt1HNbP9WSrpuHx+gB2ABmG1Mcoqm/+ugNveCpNAZGqzi1yMQ+uzl1wQS0lTMfU
|
||||||
AAABmaTDWAsAAAQDAEgwRgIhALxIgIiutEwgNcGw7/cAdjFqUugct4HlZezIOLLP
|
AAABmlkBVgcAAAQDAEcwRQIgIT/DhsIj9Aw7qf/2lknJCr907dEqC3/+QN3zlcOj
|
||||||
rg69AiEA8YCaK41rJDYztEKUIJEq2J2ktSqGYcl9gNKC+SiR4acwCgYIKoZIzj0E
|
iKoCIQCTguinYjJPZwU2dblaRQ2q7MTCMT2ZENExltxwYG3GzjAKBggqhkjOPQQD
|
||||||
AwMDaAAwZQIwVG9yOiMRfKFFyFj1R8X/5U67QD84OhZ0oM0SZsVhezLedG5b8eFf
|
AwNoADBlAjEA5nFoNrLyeC079YpRvdah/HZIA/lUBh+LOo/NcEBD3aTGs2z8hU8z
|
||||||
/cWraREi8xbFAjEA/6RXweGzl08F7EtqBDoiqitScI2rbwGtP6s/evL0zXTABZD2
|
H4vMy3OnfQ9TAjBxigm7zE5/3CAcGoSOr/P0TL52nh+lO4SUVxcbKgYB8A2yo6o/
|
||||||
ih7AGxjtg80IqIRe
|
kUkG7PiRB0uUpNw=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw
|
MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
|
||||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
|
||||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
|
||||||
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
|
||||||
RW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST
|
bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c
|
||||||
CFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef
|
S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb
|
||||||
QHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw
|
R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB
|
||||||
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
|
||||||
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4
|
MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j
|
||||||
wpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
|
cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB
|
||||||
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
|
BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE
|
||||||
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
|
DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j
|
||||||
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD
|
ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0
|
||||||
aEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF
|
RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d
|
||||||
h4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG
|
AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8
|
||||||
yM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr
|
otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA
|
||||||
OIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o
|
aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm
|
||||||
yVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S
|
Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2
|
||||||
M6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ
|
HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1
|
||||||
UXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq
|
Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR
|
||||||
Pe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I
|
xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d
|
||||||
tu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ
|
tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/
|
||||||
YRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty
|
jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS
|
||||||
+VUwFj9tmWxyR/M=
|
u1igv3OefnWjSQ==
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrfNUSjLV/7j7LSBf
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgcrhROXQT85e+S8h8
|
||||||
zL/hvGEuv+uvf3/aimqjecO7vcShRANCAATGx0o7t0pSrOoFc+pljtqJVxgaSW+w
|
RE3Z7TPo3+WA2RmzJsXJbXkbi5qhRANCAASIrTkZOM4ip42DCyDADXGc7oV3+Oki
|
||||||
9D9C2WdysMeSKKOU+0MzaM4ynLUhETOpBs8E612mdcoeak+G1Emj6UVw
|
myTM3st2RIZWG28rFRwH0LebJV2cduq1HdtlVxIEr+RhvyIL7gllueXU
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
|
## [@nhost/dashboard@2.42.0] - 2025-11-12
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(dashboard)* Datatable design improvements (#3657)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(dashboard)* Remove v2 ui components from datatable (#3568)
|
||||||
|
|
||||||
|
## [@nhost/dashboard@2.41.0] - 2025-11-04
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(auth)* Added endpoints to retrieve and refresh oauth2 providers' tokens (#3614)
|
||||||
|
- *(dashboard)* Get github repositories from github itself (#3640)
|
||||||
|
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(dashboard)* Update SQL editor to use correct hasura migrations API URL (#3645)
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ test('should create a table with nullable columns', async ({
|
|||||||
page.getByRole('link', { name: tableName, exact: true }),
|
page.getByRole('link', { name: tableName, exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await page
|
await page
|
||||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||||
.click();
|
.click();
|
||||||
await page.getByText('Edit Table').click();
|
await page.getByText('Edit Table').click();
|
||||||
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||||
@@ -143,7 +143,7 @@ test('should create a table with an identity column', async ({
|
|||||||
page.getByRole('link', { name: tableName, exact: true }),
|
page.getByRole('link', { name: tableName, exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await page
|
await page
|
||||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||||
.click();
|
.click();
|
||||||
await page.getByText('Edit Table').click();
|
await page.getByText('Edit Table').click();
|
||||||
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||||
@@ -267,7 +267,7 @@ test('should be able to create a table with a composite key', async ({
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||||
.click();
|
.click();
|
||||||
await page.getByText('Edit Table').click();
|
await page.getByText('Edit Table').click();
|
||||||
await expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
await expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ test('should create a table with role permissions to select row', async ({
|
|||||||
|
|
||||||
// Press three horizontal dots more options button next to the table name
|
// Press three horizontal dots more options button next to the table name
|
||||||
await page
|
await page
|
||||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: /edit permissions/i }).click();
|
await page.getByRole('menuitem', { name: /edit permissions/i }).click();
|
||||||
@@ -89,7 +89,7 @@ test('should create a table with role permissions and a custom check to select r
|
|||||||
|
|
||||||
// Press three horizontal dots more options button next to the table name
|
// Press three horizontal dots more options button next to the table name
|
||||||
await page
|
await page
|
||||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: /edit permissions/i }).click();
|
await page.getByRole('menuitem', { name: /edit permissions/i }).click();
|
||||||
@@ -114,7 +114,7 @@ test('should create a table with role permissions and a custom check to select r
|
|||||||
|
|
||||||
await page.getByText('Select variable...', { exact: true }).click();
|
await page.getByText('Select variable...', { exact: true }).click();
|
||||||
|
|
||||||
const variableSelector = await page.locator('input[role="combobox"]');
|
const variableSelector = page.locator('input[role="combobox"]');
|
||||||
|
|
||||||
await variableSelector.fill('X-Hasura-User-Id');
|
await variableSelector.fill('X-Hasura-User-Id');
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function getCspHeader() {
|
|||||||
return [
|
return [
|
||||||
"default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run",
|
"default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run",
|
||||||
"script-src 'self' 'unsafe-eval' cdn.segment.com js.stripe.com challenges.cloudflare.com googletagmanager.com",
|
"script-src 'self' 'unsafe-eval' cdn.segment.com js.stripe.com challenges.cloudflare.com googletagmanager.com",
|
||||||
"connect-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run discord.com api.segment.io api.segment.com cdn.segment.com nhost.zendesk.com",
|
"connect-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run discord.com api.segment.io api.segment.com cdn.segment.com nhost.zendesk.com api.github.com",
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' blob: data: github.com avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run",
|
"img-src 'self' blob: data: github.com avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run",
|
||||||
"font-src 'self' data:",
|
"font-src 'self' data:",
|
||||||
@@ -126,4 +126,4 @@ module.exports = withBundleAnalyzer({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
"@graphiql/react": "^0.22.3",
|
"@graphiql/react": "^0.22.3",
|
||||||
"@graphiql/toolkit": "^0.9.1",
|
"@graphiql/toolkit": "^0.9.1",
|
||||||
"@headlessui/react": "^1.7.18",
|
"@headlessui/react": "^1.7.18",
|
||||||
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@heroicons/react": "^1.0.6",
|
"@heroicons/react": "^1.0.6",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
"@radix-ui/react-radio-group": "^1.2.0",
|
"@radix-ui/react-radio-group": "^1.2.0",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.3",
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
@@ -93,7 +94,7 @@
|
|||||||
"just-kebab-case": "^4.2.0",
|
"just-kebab-case": "^4.2.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lucide-react": "^0.416.0",
|
"lucide-react": "^0.552.0",
|
||||||
"next": "^14.2.31",
|
"next": "^14.2.31",
|
||||||
"next-nprogress-bar": "^2.3.13",
|
"next-nprogress-bar": "^2.3.13",
|
||||||
"next-seo": "^6.5.0",
|
"next-seo": "^6.5.0",
|
||||||
|
|||||||
141
dashboard/pnpm-lock.yaml
generated
141
dashboard/pnpm-lock.yaml
generated
@@ -55,6 +55,9 @@ importers:
|
|||||||
'@headlessui/react':
|
'@headlessui/react':
|
||||||
specifier: ^1.7.18
|
specifier: ^1.7.18
|
||||||
version: 1.7.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.7.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
'@hello-pangea/dnd':
|
||||||
|
specifier: ^18.0.1
|
||||||
|
version: 18.0.1(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@heroicons/react':
|
'@heroicons/react':
|
||||||
specifier: ^1.0.6
|
specifier: ^1.0.6
|
||||||
version: 1.0.6(react@18.2.0)
|
version: 1.0.6(react@18.2.0)
|
||||||
@@ -122,8 +125,8 @@ importers:
|
|||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.2
|
||||||
version: 1.1.2(@types/react@18.2.73)(react@18.2.0)
|
version: 1.2.3(@types/react@18.2.73)(react@18.2.0)
|
||||||
'@radix-ui/react-switch':
|
'@radix-ui/react-switch':
|
||||||
specifier: ^1.2.6
|
specifier: ^1.2.6
|
||||||
version: 1.2.6(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.2.6(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
@@ -221,8 +224,8 @@ importers:
|
|||||||
specifier: ^4.0.8
|
specifier: ^4.0.8
|
||||||
version: 4.0.8
|
version: 4.0.8
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.416.0
|
specifier: ^0.552.0
|
||||||
version: 0.416.0(react@18.2.0)
|
version: 0.552.0(react@18.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: ^14.2.31
|
specifier: ^14.2.31
|
||||||
version: 14.2.32(@babel/core@7.26.10)(@playwright/test@1.54.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 14.2.32(@babel/core@7.26.10)(@playwright/test@1.54.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
@@ -2063,6 +2066,12 @@ packages:
|
|||||||
react: ^16 || ^17 || ^18
|
react: ^16 || ^17 || ^18
|
||||||
react-dom: ^16 || ^17 || ^18
|
react-dom: ^16 || ^17 || ^18
|
||||||
|
|
||||||
|
'@hello-pangea/dnd@18.0.1':
|
||||||
|
resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
'@heroicons/react@1.0.6':
|
'@heroicons/react@1.0.6':
|
||||||
resolution: {integrity: sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==}
|
resolution: {integrity: sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4048,6 +4057,9 @@ packages:
|
|||||||
'@types/urijs@1.19.25':
|
'@types/urijs@1.19.25':
|
||||||
resolution: {integrity: sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==}
|
resolution: {integrity: sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==}
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6':
|
||||||
|
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||||
|
|
||||||
'@types/uuid@9.0.8':
|
'@types/uuid@9.0.8':
|
||||||
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
|
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
|
||||||
|
|
||||||
@@ -4964,6 +4976,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
css-box-model@1.2.1:
|
||||||
|
resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
|
||||||
|
|
||||||
css.escape@1.5.1:
|
css.escape@1.5.1:
|
||||||
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||||
|
|
||||||
@@ -6547,8 +6562,8 @@ packages:
|
|||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
lucide-react@0.416.0:
|
lucide-react@0.552.0:
|
||||||
resolution: {integrity: sha512-wPWxTzdss1CTz2aqcNWNlbh4YSnH9neJWP3RaeXepxpLCTW+pmu7WcT/wxJe+Q7Y7DqGOxAqakJv0pIK3431Ag==}
|
resolution: {integrity: sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
@@ -7407,6 +7422,9 @@ packages:
|
|||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
raf-schd@4.0.3:
|
||||||
|
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
|
||||||
|
|
||||||
randombytes@2.1.0:
|
randombytes@2.1.0:
|
||||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||||
|
|
||||||
@@ -7481,6 +7499,18 @@ packages:
|
|||||||
react:
|
react:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-redux@9.2.0:
|
||||||
|
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^18.2.25 || ^19
|
||||||
|
react: ^18.0 || ^19
|
||||||
|
redux: ^5.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
redux:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-refresh@0.17.0:
|
react-refresh@0.17.0:
|
||||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -7598,6 +7628,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
redux@5.0.1:
|
||||||
|
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -8206,6 +8239,9 @@ packages:
|
|||||||
tiny-case@1.0.3:
|
tiny-case@1.0.3:
|
||||||
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
||||||
|
|
||||||
|
tiny-invariant@1.3.3:
|
||||||
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
tinybench@2.9.0:
|
tinybench@2.9.0:
|
||||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
@@ -8576,7 +8612,7 @@ packages:
|
|||||||
vite-tsconfig-paths@4.3.2:
|
vite-tsconfig-paths@4.3.2:
|
||||||
resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==}
|
resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: '>=4.5.14'
|
vite: '*'
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
@@ -8946,7 +8982,7 @@ snapshots:
|
|||||||
'@babel/core': 7.26.10
|
'@babel/core': 7.26.10
|
||||||
'@babel/generator': 7.28.3
|
'@babel/generator': 7.28.3
|
||||||
'@babel/parser': 7.28.3
|
'@babel/parser': 7.28.3
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@babel/traverse': 7.28.3
|
'@babel/traverse': 7.28.3
|
||||||
'@babel/types': 7.28.2
|
'@babel/types': 7.28.2
|
||||||
babel-preset-fbjs: 3.4.0(@babel/core@7.26.10)
|
babel-preset-fbjs: 3.4.0(@babel/core@7.26.10)
|
||||||
@@ -10094,7 +10130,7 @@ snapshots:
|
|||||||
'@emotion/babel-plugin@11.11.0':
|
'@emotion/babel-plugin@11.11.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-module-imports': 7.27.1
|
'@babel/helper-module-imports': 7.27.1
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@emotion/hash': 0.9.1
|
'@emotion/hash': 0.9.1
|
||||||
'@emotion/memoize': 0.8.1
|
'@emotion/memoize': 0.8.1
|
||||||
'@emotion/serialize': 1.1.4
|
'@emotion/serialize': 1.1.4
|
||||||
@@ -10934,6 +10970,18 @@ snapshots:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
|
||||||
|
'@hello-pangea/dnd@18.0.1(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.4
|
||||||
|
css-box-model: 1.2.1
|
||||||
|
raf-schd: 4.0.3
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
react-redux: 9.2.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1)
|
||||||
|
redux: 5.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
|
||||||
'@heroicons/react@1.0.6(react@18.2.0)':
|
'@heroicons/react@1.0.6(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
@@ -11273,7 +11321,7 @@ snapshots:
|
|||||||
|
|
||||||
'@mui/base@5.0.0-beta.40(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
'@mui/base@5.0.0-beta.40(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@floating-ui/react-dom': 2.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@floating-ui/react-dom': 2.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@mui/types': 7.2.14(@types/react@18.2.73)
|
'@mui/types': 7.2.14(@types/react@18.2.73)
|
||||||
'@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0)
|
'@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0)
|
||||||
@@ -11310,7 +11358,7 @@ snapshots:
|
|||||||
|
|
||||||
'@mui/private-theming@5.15.14(@types/react@18.2.73)(react@18.2.0)':
|
'@mui/private-theming@5.15.14(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0)
|
'@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0)
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
@@ -11319,7 +11367,7 @@ snapshots:
|
|||||||
|
|
||||||
'@mui/styled-engine@5.15.14(@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0))(@types/react@18.2.73)(react@18.2.0))(react@18.2.0)':
|
'@mui/styled-engine@5.15.14(@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0))(@types/react@18.2.73)(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@emotion/cache': 11.11.0
|
'@emotion/cache': 11.11.0
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
@@ -11350,7 +11398,7 @@ snapshots:
|
|||||||
|
|
||||||
'@mui/utils@5.15.14(@types/react@18.2.73)(react@18.2.0)':
|
'@mui/utils@5.15.14(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@types/prop-types': 15.7.12
|
'@types/prop-types': 15.7.12
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
@@ -11571,7 +11619,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/primitive@1.0.1':
|
'@radix-ui/primitive@1.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.0': {}
|
'@radix-ui/primitive@1.1.0': {}
|
||||||
|
|
||||||
@@ -11698,7 +11746,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.73
|
'@types/react': 18.2.73
|
||||||
@@ -11723,7 +11771,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-context@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
'@radix-ui/react-context@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.73
|
'@types/react': 18.2.73
|
||||||
@@ -11748,7 +11796,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
'@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/primitive': 1.0.1
|
'@radix-ui/primitive': 1.0.1
|
||||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
@@ -11799,7 +11847,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
'@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/primitive': 1.0.1
|
'@radix-ui/primitive': 1.0.1
|
||||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
@@ -11854,7 +11902,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
'@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.73
|
'@types/react': 18.2.73
|
||||||
@@ -11873,7 +11921,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
'@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
@@ -11924,7 +11972,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-id@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
'@radix-ui/react-id@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -12033,7 +12081,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
'@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
@@ -12063,7 +12111,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
'@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
@@ -12094,7 +12142,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
'@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.73)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
@@ -12257,7 +12305,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-slot@1.0.2(@types/react@18.2.73)(react@18.2.0)':
|
'@radix-ui/react-slot@1.0.2(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -12344,7 +12392,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
'@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.73
|
'@types/react': 18.2.73
|
||||||
@@ -12357,7 +12405,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
'@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -12387,7 +12435,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.73)(react@18.2.0)':
|
'@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -12402,7 +12450,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
'@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.73
|
'@types/react': 18.2.73
|
||||||
@@ -13041,6 +13089,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/urijs@1.19.25': {}
|
'@types/urijs@1.19.25': {}
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
||||||
'@types/uuid@9.0.8': {}
|
'@types/uuid@9.0.8': {}
|
||||||
|
|
||||||
'@types/validator@13.11.10': {}
|
'@types/validator@13.11.10': {}
|
||||||
@@ -13743,7 +13793,7 @@ snapshots:
|
|||||||
|
|
||||||
babel-plugin-macros@3.1.0:
|
babel-plugin-macros@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
cosmiconfig: 7.1.0
|
cosmiconfig: 7.1.0
|
||||||
resolve: 1.22.10
|
resolve: 1.22.10
|
||||||
|
|
||||||
@@ -13834,7 +13884,7 @@ snapshots:
|
|||||||
'@babel/preset-env': 7.24.7(@babel/core@7.26.10)
|
'@babel/preset-env': 7.24.7(@babel/core@7.26.10)
|
||||||
'@babel/preset-react': 7.24.6(@babel/core@7.26.10)
|
'@babel/preset-react': 7.24.6(@babel/core@7.26.10)
|
||||||
'@babel/preset-typescript': 7.24.7(@babel/core@7.26.10)
|
'@babel/preset-typescript': 7.24.7(@babel/core@7.26.10)
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
babel-plugin-macros: 3.1.0
|
babel-plugin-macros: 3.1.0
|
||||||
babel-plugin-transform-react-remove-prop-types: 0.4.24
|
babel-plugin-transform-react-remove-prop-types: 0.4.24
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -14183,6 +14233,10 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
css-box-model@1.2.1:
|
||||||
|
dependencies:
|
||||||
|
tiny-invariant: 1.3.3
|
||||||
|
|
||||||
css.escape@1.5.1: {}
|
css.escape@1.5.1: {}
|
||||||
|
|
||||||
cssesc@3.0.0: {}
|
cssesc@3.0.0: {}
|
||||||
@@ -14354,7 +14408,7 @@ snapshots:
|
|||||||
|
|
||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
domexception@4.0.0:
|
domexception@4.0.0:
|
||||||
@@ -16162,7 +16216,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
lucide-react@0.416.0(react@18.2.0):
|
lucide-react@0.552.0(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
|
||||||
@@ -17202,6 +17256,8 @@ snapshots:
|
|||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
|
raf-schd@4.0.3: {}
|
||||||
|
|
||||||
randombytes@2.1.0:
|
randombytes@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
@@ -17277,6 +17333,15 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
|
||||||
|
react-redux@9.2.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1):
|
||||||
|
dependencies:
|
||||||
|
'@types/use-sync-external-store': 0.0.6
|
||||||
|
react: 18.2.0
|
||||||
|
use-sync-external-store: 1.4.0(react@18.2.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.2.73
|
||||||
|
redux: 5.0.1
|
||||||
|
|
||||||
react-refresh@0.17.0: {}
|
react-refresh@0.17.0: {}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.8(@types/react@18.2.73)(react@18.2.0):
|
react-remove-scroll-bar@2.3.8(@types/react@18.2.73)(react@18.2.0):
|
||||||
@@ -17339,7 +17404,7 @@ snapshots:
|
|||||||
|
|
||||||
react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
dom-helpers: 5.2.1
|
dom-helpers: 5.2.1
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
@@ -17395,6 +17460,8 @@ snapshots:
|
|||||||
indent-string: 4.0.0
|
indent-string: 4.0.0
|
||||||
strip-indent: 3.0.0
|
strip-indent: 3.0.0
|
||||||
|
|
||||||
|
redux@5.0.1: {}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@@ -17429,7 +17496,7 @@ snapshots:
|
|||||||
|
|
||||||
regenerator-transform@0.15.2:
|
regenerator-transform@0.15.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
|
|
||||||
regexp.prototype.flags@1.5.3:
|
regexp.prototype.flags@1.5.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17477,7 +17544,7 @@ snapshots:
|
|||||||
|
|
||||||
relay-runtime@12.0.0(encoding@0.1.13):
|
relay-runtime@12.0.0(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.28.4
|
||||||
fbjs: 3.0.5(encoding@0.1.13)
|
fbjs: 3.0.5(encoding@0.1.13)
|
||||||
invariant: 2.2.4
|
invariant: 2.2.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -18122,6 +18189,8 @@ snapshots:
|
|||||||
|
|
||||||
tiny-case@1.0.3: {}
|
tiny-case@1.0.3: {}
|
||||||
|
|
||||||
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
tinybench@2.9.0: {}
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
tinyexec@0.3.2: {}
|
tinyexec@0.3.2: {}
|
||||||
|
|||||||
1
dashboard/react-table-config.d.ts
vendored
1
dashboard/react-table-config.d.ts
vendored
@@ -64,7 +64,6 @@ declare module 'react-table' {
|
|||||||
|
|
||||||
export interface Cell<
|
export interface Cell<
|
||||||
D extends Record<string, unknown> = Record<string, unknown>,
|
D extends Record<string, unknown> = Record<string, unknown>,
|
||||||
V = any,
|
|
||||||
> extends UseGroupByCellProps<D>,
|
> extends UseGroupByCellProps<D>,
|
||||||
UseRowStateCellProps<D> {}
|
UseRowStateCellProps<D> {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Droppable,
|
||||||
|
type DragDropContextProps,
|
||||||
|
type DroppableProps,
|
||||||
|
} from '@hello-pangea/dnd';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
type DragAndDropListProps = Omit<
|
||||||
|
DroppableProps & DragDropContextProps,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
wrapperClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DragAndDropList({
|
||||||
|
droppableId,
|
||||||
|
onDragEnd,
|
||||||
|
children,
|
||||||
|
wrapperClassName,
|
||||||
|
}: PropsWithChildren<DragAndDropListProps>) {
|
||||||
|
return (
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable droppableId={droppableId}>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
{...provided.droppableProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
className={cn(wrapperClassName)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DragAndDropList;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Draggable, type DraggableProps } from '@hello-pangea/dnd';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
export type DraggableItemProps = PropsWithChildren<
|
||||||
|
Omit<DraggableProps, 'children'> & { className?: string }
|
||||||
|
>;
|
||||||
|
|
||||||
|
function DraggableItem({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...draggableProps
|
||||||
|
}: DraggableItemProps) {
|
||||||
|
return (
|
||||||
|
<Draggable {...draggableProps}>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
className={cn(className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DraggableItem;
|
||||||
3
dashboard/src/components/common/DragAndDropList/index.ts
Normal file
3
dashboard/src/components/common/DragAndDropList/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as DragAndDropList } from './DragAndDropList';
|
||||||
|
export * from './DraggableItem';
|
||||||
|
export { default as DraggableItem } from './DraggableItem';
|
||||||
@@ -123,7 +123,7 @@ const TimePickerInput = React.forwardRef<
|
|||||||
id={id || picker}
|
id={id || picker}
|
||||||
name={name || picker}
|
name={name || picker}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-[48px] text-center font-mono text-base tabular-nums focus:bg-accent focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none',
|
'w-[48px] text-center font-mono text-base tabular-nums focus:bg-accent-background focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
value={value || calculatedValue}
|
value={value || calculatedValue}
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { Spinner } from '@/components/ui/v3/spinner';
|
||||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
import { cn } from '@/lib/utils';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
export interface FormActivityIndicatorProps extends BoxProps {}
|
export interface FormActivityIndicatorProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function FormActivityIndicator({
|
export default function FormActivityIndicator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: FormActivityIndicatorProps) {
|
}: FormActivityIndicatorProps) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={cn(
|
||||||
'grid items-center justify-center px-6 py-4',
|
'box grid h-full items-center justify-center px-6 py-4',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ActivityIndicator
|
<Spinner className="h-5 w-5" wrapperClassName="flex-row gap-1">
|
||||||
circularProgressProps={{ className: 'w-5 h-5' }}
|
Loading form...
|
||||||
label="Loading form..."
|
</Spinner>
|
||||||
/>
|
</div>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/v3/form';
|
} from '@/components/ui/v3/form';
|
||||||
import { Input } from '@/components/ui/v3/input';
|
import { Input } from '@/components/ui/v3/input';
|
||||||
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
|
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
type ChangeEvent,
|
||||||
|
type ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
import type {
|
||||||
|
Control,
|
||||||
|
ControllerRenderProps,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
PathValue,
|
||||||
|
} from 'react-hook-form';
|
||||||
|
import { mergeRefs } from 'react-merge-refs';
|
||||||
|
|
||||||
const inputClasses =
|
const inputClasses =
|
||||||
'!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500';
|
'!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500';
|
||||||
@@ -17,43 +32,116 @@ interface FormInputProps<
|
|||||||
> {
|
> {
|
||||||
control: Control<TFieldValues>;
|
control: Control<TFieldValues>;
|
||||||
name: TName;
|
name: TName;
|
||||||
label: string;
|
label: ReactNode;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
inline?: boolean;
|
||||||
|
helperText?: string | null;
|
||||||
|
transformValue?: (
|
||||||
|
value: PathValue<TFieldValues, TName>,
|
||||||
|
) => PathValue<TFieldValues, TName>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormInput<
|
function InnerFormInput<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
>({
|
>(
|
||||||
control,
|
{
|
||||||
name,
|
control,
|
||||||
label,
|
name,
|
||||||
placeholder,
|
label,
|
||||||
className = '',
|
placeholder,
|
||||||
type = 'text',
|
className = '',
|
||||||
}: FormInputProps<TFieldValues, TName>) {
|
type = 'text',
|
||||||
|
inline,
|
||||||
|
helperText,
|
||||||
|
transformValue,
|
||||||
|
}: FormInputProps<TFieldValues, TName>,
|
||||||
|
ref: ForwardedRef<HTMLInputElement>,
|
||||||
|
) {
|
||||||
|
function getOnChangeHandlerAndValue(
|
||||||
|
field: ControllerRenderProps<TFieldValues, TName>,
|
||||||
|
): [
|
||||||
|
PathValue<TFieldValues, TName>,
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => void,
|
||||||
|
] {
|
||||||
|
const { onChange, value } = field;
|
||||||
|
|
||||||
|
function handleOnChange(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
let transformedValue = event.target.value;
|
||||||
|
if (isNotEmptyValue(transformValue)) {
|
||||||
|
transformedValue = transformValue(
|
||||||
|
event.target.value as PathValue<TFieldValues, TName>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
onChange(transformedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformedValue = isNotEmptyValue(transformValue)
|
||||||
|
? transformValue(value)
|
||||||
|
: value;
|
||||||
|
|
||||||
|
return [transformedValue, handleOnChange];
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem>
|
const { onChange, value, ...fieldProps } = field;
|
||||||
<FormLabel>{label}</FormLabel>
|
|
||||||
<FormControl>
|
const [tValue, handleOnChange] = getOnChangeHandlerAndValue(field);
|
||||||
<Input
|
return (
|
||||||
type={type}
|
<FormItem
|
||||||
placeholder={placeholder || label}
|
className={cn({ 'flex w-full items-center gap-4 py-3': inline })}
|
||||||
{...field}
|
>
|
||||||
className={`${inputClasses} ${className}`}
|
<FormLabel
|
||||||
/>
|
className={cn({
|
||||||
</FormControl>
|
'mt-2 w-52 max-w-52 flex-shrink-0 self-start': inline,
|
||||||
<FormMessage />
|
})}
|
||||||
</FormItem>
|
>
|
||||||
)}
|
{label}
|
||||||
|
</FormLabel>
|
||||||
|
<div
|
||||||
|
className={cn({
|
||||||
|
'flex w-[calc(100%-13.5rem)] max-w-[calc(100%-13.5rem)] flex-col gap-2':
|
||||||
|
inline,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type={type}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
value={tValue}
|
||||||
|
{...fieldProps}
|
||||||
|
ref={mergeRefs([field.ref, ref])}
|
||||||
|
className={cn(inputClasses, className)}
|
||||||
|
wrapperClassName={cn({ 'w-full': !inline })}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{!!helperText && (
|
||||||
|
<FormDescription className="break-all px-[1px]">
|
||||||
|
{helperText}
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FormInput = forwardRef(InnerFormInput) as <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(
|
||||||
|
props: FormInputProps<TFieldValues, TName> & {
|
||||||
|
ref?: ForwardedRef<HTMLInputElement>;
|
||||||
|
},
|
||||||
|
) => ReturnType<typeof InnerFormInput>;
|
||||||
|
|
||||||
export default FormInput;
|
export default FormInput;
|
||||||
|
|||||||
125
dashboard/src/components/form/FormSelect/FormSelect.tsx
Normal file
125
dashboard/src/components/form/FormSelect/FormSelect.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/v3/form';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/v3/select';
|
||||||
|
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||||
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
import type {
|
||||||
|
Control,
|
||||||
|
ControllerRenderProps,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
PathValue,
|
||||||
|
} from 'react-hook-form';
|
||||||
|
|
||||||
|
interface FormSelectProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> {
|
||||||
|
control: Control<TFieldValues>;
|
||||||
|
name: TName;
|
||||||
|
label: ReactNode;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
inline?: boolean;
|
||||||
|
helperText?: string | null;
|
||||||
|
transformValue?: (
|
||||||
|
value: PathValue<TFieldValues, TName>,
|
||||||
|
) => PathValue<TFieldValues, TName>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormSelect<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
className = '',
|
||||||
|
inline,
|
||||||
|
helperText,
|
||||||
|
children,
|
||||||
|
transformValue,
|
||||||
|
}: PropsWithChildren<FormSelectProps<TFieldValues, TName>>) {
|
||||||
|
function getOnChangeHandlerAndValue(
|
||||||
|
field: ControllerRenderProps<TFieldValues, TName>,
|
||||||
|
): [string, (v: string) => void] {
|
||||||
|
const { onChange, value } = field;
|
||||||
|
|
||||||
|
function handleOnChange(newValue: string) {
|
||||||
|
const transformedNewValue = isNotEmptyValue(transformValue)
|
||||||
|
? transformValue(newValue as PathValue<TFieldValues, TName>)
|
||||||
|
: newValue;
|
||||||
|
|
||||||
|
onChange(transformedNewValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformedValue: string = isNotEmptyValue(transformValue)
|
||||||
|
? transformValue(value as PathValue<TFieldValues, TName>)
|
||||||
|
: value;
|
||||||
|
|
||||||
|
return [transformedValue, handleOnChange];
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { onChange, value, ...selectProps } = field;
|
||||||
|
const [tValue, handleOnChange] = getOnChangeHandlerAndValue(field);
|
||||||
|
return (
|
||||||
|
<FormItem
|
||||||
|
className={cn({ 'flex w-full items-center gap-4 py-3': inline })}
|
||||||
|
>
|
||||||
|
<FormLabel
|
||||||
|
className={cn({
|
||||||
|
'w-52 max-w-52 flex-shrink-0': inline,
|
||||||
|
'mt-2 self-start': inline && !!helperText,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</FormLabel>
|
||||||
|
<div
|
||||||
|
className={cn({
|
||||||
|
'flex w-[calc(100%-13.5rem)] max-w-[calc(100%-13.5rem)] flex-col gap-2':
|
||||||
|
inline,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
onValueChange={handleOnChange}
|
||||||
|
value={tValue}
|
||||||
|
{...selectProps}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className={className}>
|
||||||
|
<SelectValue placeholder={placeholder} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>{children}</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{!!helperText && (
|
||||||
|
<FormDescription className="break-all px-[1px]">
|
||||||
|
{helperText}
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormSelect;
|
||||||
1
dashboard/src/components/form/FormSelect/index.ts
Normal file
1
dashboard/src/components/form/FormSelect/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as FormSelect } from './FormSelect';
|
||||||
97
dashboard/src/components/form/FormTextarea/FormTextarea.tsx
Normal file
97
dashboard/src/components/form/FormTextarea/FormTextarea.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/v3/form';
|
||||||
|
import { Textarea } from '@/components/ui/v3/textarea';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { forwardRef, type ForwardedRef, type ReactNode } from 'react';
|
||||||
|
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
|
||||||
|
import { mergeRefs } from 'react-merge-refs';
|
||||||
|
|
||||||
|
const inputClasses =
|
||||||
|
'!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500';
|
||||||
|
|
||||||
|
interface FormTextareaProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> {
|
||||||
|
control: Control<TFieldValues>;
|
||||||
|
name: TName;
|
||||||
|
label: ReactNode;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
inline?: boolean;
|
||||||
|
helperText?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InnerFormTextarea<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
className = '',
|
||||||
|
inline,
|
||||||
|
helperText,
|
||||||
|
}: FormTextareaProps<TFieldValues, TName>,
|
||||||
|
ref: ForwardedRef<HTMLTextAreaElement>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem
|
||||||
|
className={cn({ 'flex w-full items-center gap-4 py-3': inline })}
|
||||||
|
>
|
||||||
|
<FormLabel
|
||||||
|
className={cn({
|
||||||
|
'mt-2 w-52 max-w-52 flex-shrink-0 self-start': inline,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</FormLabel>
|
||||||
|
<div
|
||||||
|
className={cn({
|
||||||
|
'flex w-[calc(100%-13.5rem)] max-w-[calc(100%-13.5rem)] flex-col gap-2':
|
||||||
|
inline,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...field}
|
||||||
|
ref={mergeRefs([field.ref, ref])}
|
||||||
|
className={cn(inputClasses, className)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{!!helperText && (
|
||||||
|
<FormDescription className="break-all px-[1px]">
|
||||||
|
{helperText}
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormTextarea = forwardRef(InnerFormTextarea) as <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(
|
||||||
|
props: FormTextareaProps<TFieldValues, TName> & {
|
||||||
|
ref: ForwardedRef<HTMLTextAreaElement>;
|
||||||
|
},
|
||||||
|
) => ReturnType<typeof InnerFormTextarea>;
|
||||||
|
|
||||||
|
export default FormTextarea;
|
||||||
1
dashboard/src/components/form/FormTextarea/index.ts
Normal file
1
dashboard/src/components/form/FormTextarea/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as FormTextarea } from './FormTextarea';
|
||||||
@@ -130,9 +130,12 @@ export default function AuthenticatedLayout({
|
|||||||
{withMainNav && mainNavPinned && isMdOrLarger && <PinnedMainNav />}
|
{withMainNav && mainNavPinned && isMdOrLarger && <PinnedMainNav />}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn('relative flex h-full w-full flex-row bg-accent', {
|
className={cn(
|
||||||
'overflow-x-auto': mainNavPinned && isMdOrLarger && withMainNav,
|
'bg-accent-background relative flex h-full w-full flex-row',
|
||||||
})}
|
{
|
||||||
|
'overflow-x-auto': mainNavPinned && isMdOrLarger && withMainNav,
|
||||||
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{withMainNav && (!mainNavPinned || !isMdOrLarger) && (
|
{withMainNav && (!mainNavPinned || !isMdOrLarger) && (
|
||||||
<div className="flex h-full w-6 justify-center">
|
<div className="flex h-full w-6 justify-center">
|
||||||
|
|||||||
@@ -482,9 +482,9 @@ export default function NavTree() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-8 w-full flex-row justify-start gap-1 bg-background px-1 text-foreground hover:bg-accent dark:hover:bg-muted',
|
'flex h-8 w-full flex-row justify-start gap-1 bg-background px-1 text-foreground hover:bg-accent',
|
||||||
{
|
{
|
||||||
'bg-[#ebf3ff] hover:bg-[#ebf3ff] dark:bg-muted':
|
'bg-[#ebf3ff] hover:bg-accent dark:bg-muted':
|
||||||
context.isFocused,
|
context.isFocused,
|
||||||
},
|
},
|
||||||
item.data.disabled && 'pointer-events-none opacity-50',
|
item.data.disabled && 'pointer-events-none opacity-50',
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Box } from '@/components/ui/v2/Box';
|
import { cn } from '@/lib/utils';
|
||||||
import type { TextProps } from '@/components/ui/v2/Text';
|
import type {
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
DetailedHTMLProps,
|
||||||
import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
|
ForwardedRef,
|
||||||
|
HTMLAttributes,
|
||||||
|
HTMLProps,
|
||||||
|
} from 'react';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
export type ReadOnlyToggleProps = Omit<
|
export type ReadOnlyToggleProps = Omit<
|
||||||
DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement>,
|
DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement>,
|
||||||
@@ -24,7 +26,7 @@ export type ReadOnlyToggleProps = Omit<
|
|||||||
/**
|
/**
|
||||||
* Props passed to the label.
|
* Props passed to the label.
|
||||||
*/
|
*/
|
||||||
label?: TextProps;
|
label?: HTMLAttributes<HTMLSpanElement>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,58 +38,44 @@ function ReadOnlyToggle(
|
|||||||
<span
|
<span
|
||||||
{...props}
|
{...props}
|
||||||
{...(slotProps?.root || {})}
|
{...(slotProps?.root || {})}
|
||||||
className={twMerge(
|
className={cn(
|
||||||
'inline-grid h-full w-full grid-flow-col items-center justify-start gap-1.5',
|
'inline-grid h-full w-full grid-flow-col items-center justify-start gap-1.5',
|
||||||
slotProps?.root?.className,
|
slotProps?.root?.className,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<Box
|
<span
|
||||||
component="span"
|
className={cn(
|
||||||
sx={{
|
'box-border inline-grid h-3 w-5 items-center rounded-full border-1 border-primary-text bg-transparent px-0.5',
|
||||||
backgroundColor: (theme) => {
|
checked && 'justify-end',
|
||||||
if (checked) {
|
{
|
||||||
return theme.palette.mode === 'dark' ? 'grey.400' : 'grey.700';
|
'border-transparent bg-primary-text px-0.5 dark:bg-[#363a43]':
|
||||||
}
|
checked,
|
||||||
|
|
||||||
return 'transparent';
|
|
||||||
},
|
},
|
||||||
borderColor: checked ? 'transparent' : 'grey.700',
|
|
||||||
}}
|
|
||||||
className={twMerge(
|
|
||||||
'box-border inline-grid h-3 w-5 items-center rounded-full border-1 px-0.5',
|
|
||||||
checked === true && 'justify-end',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Box
|
<span
|
||||||
component="span"
|
className={cn(
|
||||||
sx={{
|
'inline-block h-2 w-2 rounded-full border-primary-text bg-primary-text',
|
||||||
backgroundColor: (theme) => {
|
{
|
||||||
if (checked) {
|
'border-transparent bg-data-cell-bg px-0.5 dark:bg-[#f4f7f9]':
|
||||||
return theme.palette.mode === 'dark' ? 'grey.700' : 'grey.200';
|
checked,
|
||||||
}
|
'my-px h-px justify-self-center': checked === null,
|
||||||
|
|
||||||
return 'grey.700';
|
|
||||||
},
|
},
|
||||||
}}
|
|
||||||
className={twMerge(
|
|
||||||
'inline-block h-2 w-2 rounded-full',
|
|
||||||
checked === null && 'my-px h-px justify-self-center',
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</span>
|
||||||
|
|
||||||
<Text
|
<span
|
||||||
{...(slotProps?.label || {})}
|
{...(slotProps?.label || {})}
|
||||||
component="span"
|
className={cn(
|
||||||
className={twMerge(
|
|
||||||
'truncate !text-xs font-normal',
|
'truncate !text-xs font-normal',
|
||||||
slotProps?.label?.className,
|
slotProps?.label?.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{String(checked)}
|
{String(checked)}
|
||||||
</Text>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import type { IconProps } from '@/components/ui/v2/icons';
|
|
||||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
|
||||||
|
|
||||||
function KeyIcon(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SvgIcon
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
aria-label="Key"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M5.823 7.677a4.496 4.496 0 1 1 2.5 2.5L7.5 11H6v1.5H4.5V14H2v-2.5l3.823-3.823Z"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<path
|
|
||||||
d="M10.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</SvgIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyIcon.displayName = 'NhostKeyIcon';
|
|
||||||
|
|
||||||
export default KeyIcon;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as KeyIcon } from './KeyIcon';
|
|
||||||
@@ -11,7 +11,7 @@ const badgeVariants = cva(
|
|||||||
default:
|
default:
|
||||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80 dark:text-white',
|
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80 dark:text-white',
|
||||||
secondary:
|
secondary:
|
||||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
'border-transparent bg-card text-secondary-foreground hover:bg-secondary/80',
|
||||||
destructive:
|
destructive:
|
||||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||||
outline: 'text-foreground',
|
outline: 'text-foreground',
|
||||||
|
|||||||
83
dashboard/src/components/ui/v3/button-group.tsx
Normal file
83
dashboard/src/components/ui/v3/button-group.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Separator } from "@/components/ui/v3/separator"
|
||||||
|
|
||||||
|
const buttonGroupVariants = cva(
|
||||||
|
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
horizontal:
|
||||||
|
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||||
|
vertical:
|
||||||
|
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: "horizontal",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function ButtonGroup({
|
||||||
|
className,
|
||||||
|
orientation,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
data-slot="button-group"
|
||||||
|
data-orientation={orientation}
|
||||||
|
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonGroupText({
|
||||||
|
className,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "div"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(
|
||||||
|
"bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonGroupSeparator({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
|
return (
|
||||||
|
<Separator
|
||||||
|
data-slot="button-group-separator"
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ButtonGroup,
|
||||||
|
ButtonGroupSeparator,
|
||||||
|
ButtonGroupText,
|
||||||
|
buttonGroupVariants,
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@ const buttonVariants = cva(
|
|||||||
outline:
|
outline:
|
||||||
'border bg-background hover:bg-accent hover:text-accent-foreground',
|
'border bg-background hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary:
|
secondary:
|
||||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
'bg-secondary text-secondary-foreground hover:bg-secondary-hover',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent text-accent-foreground',
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ const Checkbox = React.forwardRef<
|
|||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:border-disabled disabled:bg-disabled disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
className={cn('flex items-center justify-center text-current')}
|
className={cn('flex items-center justify-center text-white')}
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<Check width={16} height={16} strokeWidth={3} />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ interface DialogContentProps
|
|||||||
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
||||||
disableOutsideClick?: boolean;
|
disableOutsideClick?: boolean;
|
||||||
hideCloseButton?: boolean;
|
hideCloseButton?: boolean;
|
||||||
|
closeButtonClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
@@ -38,7 +39,14 @@ const DialogContent = React.forwardRef<
|
|||||||
DialogContentProps
|
DialogContentProps
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{ className, children, disableOutsideClick, hideCloseButton, ...props },
|
{
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
disableOutsideClick,
|
||||||
|
hideCloseButton,
|
||||||
|
closeButtonClassName,
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref,
|
ref,
|
||||||
) => (
|
) => (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
@@ -58,7 +66,12 @@ const DialogContent = React.forwardRef<
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{!hideCloseButton && (
|
{!hideCloseButton && (
|
||||||
<DialogPrimitive.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-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close
|
||||||
|
className={cn(
|
||||||
|
'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-accent-background data-[state=open]:text-muted-foreground',
|
||||||
|
closeButtonClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ export function InlineCode({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: PropsWithChildren<{ className?: string }>) {
|
}: PropsWithChildren<React.HTMLAttributes<HTMLElement>>) {
|
||||||
return (
|
return (
|
||||||
<code
|
<code
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative rounded bg-[#eaedf0] px-1 font-mono text-[11px] dark:bg-[#2f363d]',
|
'relative max-w-xs truncate rounded bg-[#eaedf0] px-1 font-mono text-[11px] dark:bg-[#2f363d]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { cn } from '@/lib/utils';
|
|||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||||
prefix?: React.ReactNode;
|
prefix?: React.ReactNode;
|
||||||
|
wrapperClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, prefix, ...props }, ref) => {
|
({ className, type, prefix, wrapperClassName, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center">
|
<div className={cn('relative flex items-center', wrapperClassName)}>
|
||||||
{prefix && (
|
{prefix && (
|
||||||
<span className="pointer-events-none absolute left-3 flex items-center text-muted-foreground">
|
<span className="pointer-events-none absolute left-3 flex items-center text-muted-foreground">
|
||||||
{prefix}
|
{prefix}
|
||||||
@@ -19,7 +20,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent',
|
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent-background',
|
||||||
{ 'pl-6': prefix },
|
{ 'pl-6': prefix },
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ interface SheetContentProps
|
|||||||
VariantProps<typeof sheetVariants> {
|
VariantProps<typeof sheetVariants> {
|
||||||
container?: HTMLElement | null;
|
container?: HTMLElement | null;
|
||||||
hideCloseButton?: boolean;
|
hideCloseButton?: boolean;
|
||||||
|
showOverlay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SheetContent = React.forwardRef<
|
const SheetContent = React.forwardRef<
|
||||||
@@ -67,11 +68,13 @@ const SheetContent = React.forwardRef<
|
|||||||
container = null,
|
container = null,
|
||||||
hideCloseButton,
|
hideCloseButton,
|
||||||
children,
|
children,
|
||||||
|
showOverlay = false,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => (
|
) => (
|
||||||
<SheetPortal container={container}>
|
<SheetPortal container={container}>
|
||||||
|
{showOverlay && <SheetOverlay />}
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(sheetVariants({ side }), className)}
|
className={cn(sheetVariants({ side }), className)}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface SpinnerContentProps
|
|||||||
VariantProps<typeof loaderVariants> {
|
VariantProps<typeof loaderVariants> {
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
wrapperClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Spinner({
|
export function Spinner({
|
||||||
@@ -40,10 +41,12 @@ export function Spinner({
|
|||||||
show,
|
show,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
wrapperClassName,
|
||||||
}: SpinnerContentProps) {
|
}: SpinnerContentProps) {
|
||||||
return (
|
return (
|
||||||
<span className={spinnerVariants({ show })}>
|
<span className={cn(spinnerVariants({ show }), wrapperClassName)}>
|
||||||
<Loader2
|
<Loader2
|
||||||
|
role="progressbar"
|
||||||
className={cn(
|
className={cn(
|
||||||
loaderVariants({ size }),
|
loaderVariants({ size }),
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function SocialProvidersSettings() {
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
return nhost.auth.signInProviderURL('github', {
|
return nhost.auth.signInProviderURL('github', {
|
||||||
connect: token,
|
connect: token,
|
||||||
redirectTo: `${window.location.origin}/account`,
|
redirectTo: `${window.location.origin}/account?signinProvider=github`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -3,18 +3,21 @@ import {
|
|||||||
useGithubAuthentication,
|
useGithubAuthentication,
|
||||||
type UseGithubAuthenticationHookProps,
|
type UseGithubAuthenticationHookProps,
|
||||||
} from '@/features/auth/AuthProviders/Github/hooks/useGithubAuthentication';
|
} from '@/features/auth/AuthProviders/Github/hooks/useGithubAuthentication';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { SiGithub } from '@icons-pack/react-simple-icons';
|
import { SiGithub } from '@icons-pack/react-simple-icons';
|
||||||
|
|
||||||
interface Props extends UseGithubAuthenticationHookProps {
|
interface Props extends UseGithubAuthenticationHookProps {
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
withAnonId?: boolean;
|
withAnonId?: boolean;
|
||||||
redirectTo?: string;
|
redirectTo?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function GithubAuthButton({
|
function GithubAuthButton({
|
||||||
buttonText = 'Continue with GitHub',
|
buttonText = 'Continue with GitHub',
|
||||||
withAnonId = false,
|
withAnonId = false,
|
||||||
redirectTo,
|
redirectTo,
|
||||||
|
className,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { mutate: signInWithGithub, isLoading } = useGithubAuthentication({
|
const { mutate: signInWithGithub, isLoading } = useGithubAuthentication({
|
||||||
withAnonId,
|
withAnonId,
|
||||||
@@ -22,7 +25,10 @@ function GithubAuthButton({
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="gap-2 !bg-white text-sm+ !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60"
|
className={cn(
|
||||||
|
'gap-2 !bg-white text-sm+ !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onClick={() => signInWithGithub()}
|
onClick={() => signInWithGithub()}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ function useGithubAuthentication({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectURl = nhost.auth.signInProviderURL('github', options);
|
const redirectURL = nhost.auth.signInProviderURL('github', options);
|
||||||
window.location.href = redirectURl;
|
window.location.href = redirectURL;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ function SignInWithEmailAndPassword({ onSubmit, isLoading }: Props) {
|
|||||||
label="Email"
|
label="Email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
control={form.control}
|
control={form.control}
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
<NextLink
|
<NextLink
|
||||||
href="/password/new"
|
href="/password/new"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/component
|
|||||||
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
||||||
|
|
||||||
function SignInWithGithub() {
|
function SignInWithGithub() {
|
||||||
const redirectTo = useHostName();
|
const redirectTo = `${useHostName()}?signinProvider=github`;
|
||||||
return (
|
return (
|
||||||
<GithubAuthButton
|
<GithubAuthButton
|
||||||
redirectTo={redirectTo}
|
redirectTo={redirectTo}
|
||||||
|
|||||||
@@ -22,18 +22,25 @@ function SignUpWithEmailAndPasswordForm() {
|
|||||||
onSubmit={form.handleSubmit(onSignUpWithPassword)}
|
onSubmit={form.handleSubmit(onSignUpWithPassword)}
|
||||||
className="grid grid-flow-row gap-4 bg-transparent"
|
className="grid grid-flow-row gap-4 bg-transparent"
|
||||||
>
|
>
|
||||||
<FormInput control={form.control} label="Name" name="displayName" />
|
<FormInput
|
||||||
|
control={form.control}
|
||||||
|
label="Name"
|
||||||
|
name="displayName"
|
||||||
|
placeholder="Name"
|
||||||
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
control={form.control}
|
control={form.control}
|
||||||
label="Email"
|
label="Email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
control={form.control}
|
control={form.control}
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -22,12 +22,18 @@ function SignUpWithSecurityKeyForm() {
|
|||||||
onSubmit={form.handleSubmit(onSignUpWithSecurityKey)}
|
onSubmit={form.handleSubmit(onSignUpWithSecurityKey)}
|
||||||
className="grid grid-flow-row gap-4 bg-transparent"
|
className="grid grid-flow-row gap-4 bg-transparent"
|
||||||
>
|
>
|
||||||
<FormInput control={form.control} label="Name" name="displayName" />
|
<FormInput
|
||||||
|
control={form.control}
|
||||||
|
label="Name"
|
||||||
|
name="displayName"
|
||||||
|
placeholder="Name"
|
||||||
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
control={form.control}
|
control={form.control}
|
||||||
label="Email"
|
label="Email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
|
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
|
||||||
|
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
||||||
|
|
||||||
function SignUpWithGithub() {
|
function SignUpWithGithub() {
|
||||||
|
const redirectTo = `${useHostName()}?signinProvider=github`;
|
||||||
return (
|
return (
|
||||||
<GithubAuthButton
|
<GithubAuthButton
|
||||||
|
redirectTo={redirectTo}
|
||||||
buttonText="Sign Up with GitHub"
|
buttonText="Sign Up with GitHub"
|
||||||
errorText="An error occurred while trying to sign up using GitHub. Please try again."
|
errorText="An error occurred while trying to sign up using GitHub. Please try again."
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function BillingDetails() {
|
|||||||
<AccordionContent className="border-t-1 pb-0">
|
<AccordionContent className="border-t-1 pb-0">
|
||||||
<div className="rounded-md">
|
<div className="rounded-md">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="w-full bg-accent">
|
<TableHeader className="w-full bg-accent-background">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead colSpan={3} className="w-full rounded-tl-md">
|
<TableHead colSpan={3} className="w-full rounded-tl-md">
|
||||||
Item
|
Item
|
||||||
@@ -72,7 +72,7 @@ export default function BillingDetails() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter className="bg-accent">
|
<TableFooter className="bg-accent-background">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="rounded-bl-md">
|
<TableCell colSpan={3} className="rounded-bl-md">
|
||||||
Total
|
Total
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function ProjectsGrid({ projects }: ProjectGridProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto h-full overflow-auto bg-accent">
|
<div className="mx-auto h-full overflow-auto bg-accent-background">
|
||||||
<div className="flex w-full flex-shrink-0 flex-row items-center justify-between gap-2 border-b bg-background p-2">
|
<div className="flex w-full flex-shrink-0 flex-row items-center justify-between gap-2 border-b bg-background p-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Find Project"
|
placeholder="Find Project"
|
||||||
@@ -85,7 +85,6 @@ export default function ProjectsGrid({ projects }: ProjectGridProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
{filteredProjects.map((project) => (
|
{filteredProjects.map((project) => (
|
||||||
<ProjectCard key={project.id} project={project} />
|
<ProjectCard key={project.id} project={project} />
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { DragAndDropList } from '@/components/common/DragAndDropList';
|
||||||
|
|
||||||
|
import { isEmptyValue } from '@/lib/utils';
|
||||||
|
import type { DropResult } from '@hello-pangea/dnd';
|
||||||
|
import type { ColumnInstance } from 'react-table';
|
||||||
|
import ColumnCustomizerRow from './ColumnCustomizerRow';
|
||||||
|
import ShowHideAllColumnsButtons from './ShowHideAllColumnsButtons';
|
||||||
|
|
||||||
|
type ColumnCustomizerProps = {
|
||||||
|
columns: ColumnInstance[];
|
||||||
|
onDragEnd: (columnsOrder: string[]) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
onShowAllColumns: () => void;
|
||||||
|
onHideAllColumns: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function reorder(list: ColumnInstance[], startIndex: number, endIndex: number) {
|
||||||
|
const result = Array.from(list);
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColumnCustomizer({
|
||||||
|
columns,
|
||||||
|
onDragEnd,
|
||||||
|
onReset,
|
||||||
|
onShowAllColumns,
|
||||||
|
onHideAllColumns,
|
||||||
|
}: ColumnCustomizerProps) {
|
||||||
|
function handleDragEnd(result: DropResult) {
|
||||||
|
if (isEmptyValue(result.destination)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reordered = reorder(
|
||||||
|
columns,
|
||||||
|
result.source.index,
|
||||||
|
result.destination!.index,
|
||||||
|
).map(({ id }) => id);
|
||||||
|
|
||||||
|
onDragEnd(reordered);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex max-h-[calc(100%-15rem)] flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h4 className="font-medium leading-none">Column Settings</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Reorder columns by dragging or show/hide them with checkboxes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="self-center">
|
||||||
|
<ShowHideAllColumnsButtons
|
||||||
|
onShowAll={onShowAllColumns}
|
||||||
|
onHideAll={onHideAllColumns}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-scroll">
|
||||||
|
<DragAndDropList droppableId="columnOrder" onDragEnd={handleDragEnd}>
|
||||||
|
{columns.map((column, index) => (
|
||||||
|
<ColumnCustomizerRow
|
||||||
|
key={column.id}
|
||||||
|
column={column}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DragAndDropList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColumnCustomizer;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
DraggableItem,
|
||||||
|
type DraggableItemProps,
|
||||||
|
} from '@/components/common/DragAndDropList';
|
||||||
|
import { Checkbox } from '@/components/ui/v3/checkbox';
|
||||||
|
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
|
||||||
|
import PersistenDataTableConfigurationStorage from '@/features/orgs/projects/storage/dataGrid/utils/PersistenDataTableConfigurationStorage';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { GripVertical } from 'lucide-react';
|
||||||
|
import type { ColumnInstance } from 'react-table';
|
||||||
|
|
||||||
|
type ColumnCustomizerProps = {
|
||||||
|
column: ColumnInstance;
|
||||||
|
} & Omit<DraggableItemProps, 'draggableId'>;
|
||||||
|
|
||||||
|
function ColumnCustomizerRow({ column, index }: ColumnCustomizerProps) {
|
||||||
|
const tablePath = useTablePath();
|
||||||
|
|
||||||
|
function handleVisibilityChange() {
|
||||||
|
PersistenDataTableConfigurationStorage.toggleColumnVisibility(
|
||||||
|
tablePath,
|
||||||
|
column.id,
|
||||||
|
);
|
||||||
|
column.toggleHidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraggableItem draggableId={column.id} index={index} className="mb-3">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex w-full items-center justify-between rounded-md bg-accent p-2',
|
||||||
|
{ 'opacity-70': !column.isVisible },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<Checkbox
|
||||||
|
checked={column.isVisible}
|
||||||
|
className="h-[1.125rem] w-[1.125rem] border-[#21324b] data-[state=checked]:!border-transparent dark:border-[#dfecf5]"
|
||||||
|
onCheckedChange={handleVisibilityChange}
|
||||||
|
/>
|
||||||
|
<span>{column.id}</span>
|
||||||
|
</div>
|
||||||
|
<GripVertical />
|
||||||
|
</div>
|
||||||
|
</DraggableItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColumnCustomizerRow;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Button } from '@/components/ui/v3/button';
|
||||||
|
import { ButtonGroup } from '@/components/ui/v3/button-group';
|
||||||
|
|
||||||
|
type ShowHideAllColumnsToggleProps = {
|
||||||
|
onShowAll: () => void;
|
||||||
|
onHideAll: () => void;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ShowHideAllColumnsButtons({
|
||||||
|
onShowAll,
|
||||||
|
onHideAll,
|
||||||
|
onReset,
|
||||||
|
}: ShowHideAllColumnsToggleProps) {
|
||||||
|
return (
|
||||||
|
<ButtonGroup className="w-full">
|
||||||
|
<Button variant="outline" className="flex-1" onClick={onShowAll}>
|
||||||
|
Show all columns
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="flex-1" onClick={onHideAll}>
|
||||||
|
Hide all columns
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="flex-1" onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShowHideAllColumnsButtons;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as ColumnCustomizer } from './ColumnCustomizer';
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '@/components/ui/v3/sheet';
|
||||||
|
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
|
||||||
|
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||||
|
import PersistenDataTableConfigurationStorage from '@/features/orgs/projects/storage/dataGrid/utils/PersistenDataTableConfigurationStorage';
|
||||||
|
import { ColumnCustomizer } from './ColumnCustomizer';
|
||||||
|
import { useDataGridCustomizerOpenStateContext } from './DataGridCustomizerOpenStateProvider';
|
||||||
|
import DataGridCustomizerTrigger from './DataGridCustomizerTrigger';
|
||||||
|
import RowDensityCustomizer from './RowDensityCustomizer';
|
||||||
|
|
||||||
|
function DataGridCustomizerControls() {
|
||||||
|
const { allColumns, setColumnOrder, setHiddenColumns } = useDataGridConfig();
|
||||||
|
const tablePath = useTablePath();
|
||||||
|
const { open, setOpen } = useDataGridCustomizerOpenStateContext();
|
||||||
|
|
||||||
|
const columns = allColumns.filter(({ id }) => id !== 'selection-column');
|
||||||
|
|
||||||
|
function saveHiddenCols(cols: string[]) {
|
||||||
|
setHiddenColumns(cols);
|
||||||
|
PersistenDataTableConfigurationStorage.saveHiddenColumns(tablePath, cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOriginalOrder() {
|
||||||
|
setColumnOrder([]);
|
||||||
|
PersistenDataTableConfigurationStorage.saveColumnOrder(tablePath, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
showOriginalOrder();
|
||||||
|
saveHiddenCols([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd(newOrder: string[]) {
|
||||||
|
setColumnOrder(newOrder);
|
||||||
|
PersistenDataTableConfigurationStorage.saveColumnOrder(tablePath, newOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAllColumns() {
|
||||||
|
const allColumnsId = columns.map(({ id }) => id);
|
||||||
|
saveHiddenCols(allColumnsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<DataGridCustomizerTrigger />
|
||||||
|
</SheetTrigger>
|
||||||
|
|
||||||
|
<SheetContent
|
||||||
|
className="box flex w-full flex-col rounded-none md:w-[26rem] md:max-w-[26rem]"
|
||||||
|
onInteractOutside={() => setOpen(false)}
|
||||||
|
showOverlay
|
||||||
|
>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>Customize Table View</SheetTitle>
|
||||||
|
<SheetDescription className="sr-only">
|
||||||
|
Customize columns
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<div className="flex h-full flex-col gap-8">
|
||||||
|
<RowDensityCustomizer />
|
||||||
|
<ColumnCustomizer
|
||||||
|
columns={columns}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onReset={handleReset}
|
||||||
|
onShowAllColumns={() => saveHiddenCols([])}
|
||||||
|
onHideAllColumns={hideAllColumns}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DataGridCustomizerControls;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type Dispatch,
|
||||||
|
type PropsWithChildren,
|
||||||
|
type SetStateAction,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
type DataGridCustomizerOpenStateContextProps = {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DataGridCustomizerOpenStateContext =
|
||||||
|
createContext<DataGridCustomizerOpenStateContextProps>({
|
||||||
|
open: false,
|
||||||
|
setOpen: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
function DataGridCustomizerOpenStateProvider({ children }: PropsWithChildren) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
}),
|
||||||
|
[open],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<DataGridCustomizerOpenStateContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</DataGridCustomizerOpenStateContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDataGridCustomizerOpenStateContext() {
|
||||||
|
const context = useContext(DataGridCustomizerOpenStateContext);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DataGridCustomizerOpenStateProvider;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Button, type ButtonProps } from '@/components/ui/v3/button';
|
||||||
|
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Columns3 } from 'lucide-react';
|
||||||
|
import { type ForwardedRef, forwardRef } from 'react';
|
||||||
|
|
||||||
|
function DataBrowserCustomizerTrigger(
|
||||||
|
props: ButtonProps,
|
||||||
|
ref: ForwardedRef<HTMLButtonElement>,
|
||||||
|
) {
|
||||||
|
const { allColumns } = useDataGridConfig();
|
||||||
|
const numberOfHiddenColumns = allColumns.filter(
|
||||||
|
({ isVisible }) => !isVisible,
|
||||||
|
).length;
|
||||||
|
const hasHiddenColumns = numberOfHiddenColumns !== 0;
|
||||||
|
|
||||||
|
const { className, ...buttonProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className={cn('relative', className)}
|
||||||
|
{...(hasHiddenColumns && {
|
||||||
|
title: `${numberOfHiddenColumns} ${numberOfHiddenColumns === 1 ? ' column is' : ' columns are'} hidden`,
|
||||||
|
})}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
<Columns3 />
|
||||||
|
{hasHiddenColumns && (
|
||||||
|
<span className="absolute bottom-[8px] right-[6px] w-[0.625rem] rounded-full bg-primary-text p-0 text-[0.625rem] leading-none text-paper">
|
||||||
|
!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(DataBrowserCustomizerTrigger);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Label } from '@/components/ui/v3/label';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/v3/radio-group';
|
||||||
|
import { useDataTableDesignContext } from '@/features/orgs/projects/storage/dataGrid/providers/DataTableDesignProvider';
|
||||||
|
|
||||||
|
function RowDensityCustomizer() {
|
||||||
|
const context = useDataTableDesignContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 pt-2">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h4 className="font-medium leading-none">Density</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Set row height across all tables
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<RadioGroup
|
||||||
|
className="flex flex-col space-y-1"
|
||||||
|
defaultValue={context.rowDensity}
|
||||||
|
value={context.rowDensity}
|
||||||
|
onValueChange={context.setRowDensity}
|
||||||
|
>
|
||||||
|
<div className="flex justify-start gap-3">
|
||||||
|
<RadioGroupItem value="comfortable" id="height1" />
|
||||||
|
<Label htmlFor="height1" className="hover:cursor-pointer">
|
||||||
|
Comfortable
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-start gap-3">
|
||||||
|
<RadioGroupItem value="compact" id="height2" />
|
||||||
|
<Label htmlFor="height2" className="hover:cursor-pointer">
|
||||||
|
Compact
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RowDensityCustomizer;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as DataGridCustomizerControls } from './DataGridCustomizerControls';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export type RowDensity = 'comfortable' | 'compact';
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import { Form } from '@/components/form/Form';
|
import { Form } from '@/components/form/Form';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
|
||||||
import { DatabaseRecordInputGroup } from '@/features/orgs/projects/database/dataGrid/components/DatabaseRecordInputGroup';
|
import { DatabaseRecordInputGroup } from '@/features/orgs/projects/database/dataGrid/components/DatabaseRecordInputGroup';
|
||||||
import type {
|
import type {
|
||||||
ColumnInsertOptions,
|
ColumnInsertOptions,
|
||||||
DataBrowserGridColumn,
|
DataBrowserGridColumn,
|
||||||
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import type { DialogFormProps } from '@/types/common';
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
@@ -153,16 +153,19 @@ export default function BaseRecordForm({
|
|||||||
description="These columns are nullable and don't require a value."
|
description="These columns are nullable and don't require a value."
|
||||||
columns={optionalColumns}
|
columns={optionalColumns}
|
||||||
autoFocusFirstInput={requiredColumns.length === 0}
|
autoFocusFirstInput={requiredColumns.length === 0}
|
||||||
sx={{ borderTopWidth: requiredColumns.length > 0 ? 1 : 0 }}
|
className={cn(
|
||||||
className="px-6 pt-3"
|
'px-6 pt-3',
|
||||||
|
requiredColumns.length > 0 ? 'border-t-1' : 'border-t-0',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Box className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
|
<div className="box grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="borderless"
|
variant="outline"
|
||||||
color="secondary"
|
className="border-none"
|
||||||
|
size="sm"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
tabIndex={isDirty ? -1 : 0}
|
tabIndex={isDirty ? -1 : 0}
|
||||||
>
|
>
|
||||||
@@ -172,12 +175,13 @@ export default function BaseRecordForm({
|
|||||||
<Button
|
<Button
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="justify-self-end"
|
className="justify-self-end"
|
||||||
>
|
>
|
||||||
{submitButtonText}
|
{submitButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ export default function ColumnEditorTable() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div role="table" className="col-span-8 overflow-x-auto">
|
<div
|
||||||
|
role="table"
|
||||||
|
className="col-span-8 overflow-x-auto min-[900px]:overflow-x-visible"
|
||||||
|
>
|
||||||
<div className="sticky top-0 z-10 flex w-full gap-2 pb-2 pt-1">
|
<div className="sticky top-0 z-10 flex w-full gap-2 pb-2 pt-1">
|
||||||
<div role="columnheader" className="w-52 flex-none">
|
<div role="columnheader" className="w-52 flex-none">
|
||||||
<InputLabel as="span">
|
<InputLabel as="span">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Alert } from '@/components/ui/v2/Alert';
|
import { Alert } from '@/components/ui/v3/alert';
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
import { Button } from '@/components/ui/v3/button';
|
||||||
|
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
|
||||||
import type { BaseRecordFormProps } from '@/features/orgs/projects/database/dataGrid/components/BaseRecordForm';
|
import type { BaseRecordFormProps } from '@/features/orgs/projects/database/dataGrid/components/BaseRecordForm';
|
||||||
import { BaseRecordForm } from '@/features/orgs/projects/database/dataGrid/components/BaseRecordForm';
|
import { BaseRecordForm } from '@/features/orgs/projects/database/dataGrid/components/BaseRecordForm';
|
||||||
import { useCreateRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useCreateRecordMutation';
|
import { useCreateRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useCreateRecordMutation';
|
||||||
@@ -7,6 +8,7 @@ import type { ColumnInsertOptions } from '@/features/orgs/projects/database/data
|
|||||||
import { createDynamicValidationSchema } from '@/features/orgs/projects/database/dataGrid/utils/validationSchemaHelpers';
|
import { createDynamicValidationSchema } from '@/features/orgs/projects/database/dataGrid/utils/validationSchemaHelpers';
|
||||||
import { triggerToast } from '@/utils/toast';
|
import { triggerToast } from '@/utils/toast';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
export interface CreateRecordFormProps
|
export interface CreateRecordFormProps
|
||||||
@@ -15,14 +17,20 @@ export interface CreateRecordFormProps
|
|||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
onSubmit?: (args?: any) => Promise<any>;
|
onSubmit?: (args?: any) => Promise<any>;
|
||||||
|
currentOffset: number;
|
||||||
|
sortByString: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateRecordForm({
|
export default function CreateRecordForm({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
currentOffset,
|
||||||
|
sortByString,
|
||||||
...props
|
...props
|
||||||
}: CreateRecordFormProps) {
|
}: CreateRecordFormProps) {
|
||||||
const { mutateAsync: insertRow, error, reset } = useCreateRecordMutation();
|
const { mutateAsync: insertRow, error, reset } = useCreateRecordMutation();
|
||||||
const validationSchema = createDynamicValidationSchema(props.columns);
|
const validationSchema = createDynamicValidationSchema(props.columns);
|
||||||
|
const currentTablePath = useTablePath();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: props.columns.reduce((defaultValues, column) => {
|
defaultValues: props.columns.reduce((defaultValues, column) => {
|
||||||
@@ -42,6 +50,12 @@ export default function CreateRecordForm({
|
|||||||
|
|
||||||
if (onSubmit) {
|
if (onSubmit) {
|
||||||
await onSubmit();
|
await onSubmit();
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [currentTablePath, currentOffset],
|
||||||
|
});
|
||||||
|
await queryClient.refetchQueries({
|
||||||
|
queryKey: [currentTablePath, currentOffset],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerToast('The row has been inserted successfully.');
|
triggerToast('The row has been inserted successfully.');
|
||||||
@@ -55,18 +69,17 @@ export default function CreateRecordForm({
|
|||||||
{error && error instanceof Error ? (
|
{error && error instanceof Error ? (
|
||||||
<div className="-mt-3 mb-4 px-6">
|
<div className="-mt-3 mb-4 px-6">
|
||||||
<Alert
|
<Alert
|
||||||
severity="error"
|
variant="destructive"
|
||||||
className="grid grid-flow-col items-center justify-between px-4 py-3"
|
className="grid grid-flow-col items-center justify-between border-none bg-[#f1315433] px-4 py-3"
|
||||||
>
|
>
|
||||||
<span className="text-left">
|
<span className="text-left">
|
||||||
<strong>Error:</strong> {error.message}
|
<strong>Error:</strong> {error.message}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="borderless"
|
|
||||||
color="error"
|
|
||||||
size="small"
|
|
||||||
onClick={reset}
|
onClick={reset}
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
className="bg-transparent text-[#c91737] hover:bg-[#f131541a]"
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Text } from '@/components/ui/v2/Text';
|
import { cn } from '@/lib/utils';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import type { DetailedHTMLProps, HTMLProps, ReactNode } from 'react';
|
import type { DetailedHTMLProps, HTMLProps, ReactNode } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
export interface DataBrowserEmptyStateProps
|
export interface DataBrowserEmptyStateProps
|
||||||
extends Omit<
|
extends Omit<
|
||||||
@@ -26,7 +25,7 @@ export default function DataBrowserEmptyState({
|
|||||||
}: DataBrowserEmptyStateProps) {
|
}: DataBrowserEmptyStateProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={cn(
|
||||||
'grid w-full place-content-center gap-2 px-4 py-16 text-center',
|
'grid w-full place-content-center gap-2 px-4 py-16 text-center',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -41,12 +40,10 @@ export default function DataBrowserEmptyState({
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<h1 className="font-inter-var text-[1.125rem] font-medium !leading-6">
|
||||||
<Text variant="h3" component="h1">
|
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</h1>
|
||||||
|
<p>{description}</p>
|
||||||
<Text>{description}</Text>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ import { normalizeDefaultValue } from '@/features/orgs/projects/database/dataGri
|
|||||||
import {
|
import {
|
||||||
POSTGRESQL_CHARACTER_TYPES,
|
POSTGRESQL_CHARACTER_TYPES,
|
||||||
POSTGRESQL_DATE_TIME_TYPES,
|
POSTGRESQL_DATE_TIME_TYPES,
|
||||||
POSTGRESQL_DECIMAL_TYPES,
|
|
||||||
POSTGRESQL_INTEGER_TYPES,
|
|
||||||
POSTGRESQL_JSON_TYPES,
|
POSTGRESQL_JSON_TYPES,
|
||||||
|
POSTGRESQL_NUMERIC_TYPES,
|
||||||
} from '@/features/orgs/projects/database/dataGrid/utils/postgresqlConstants';
|
} from '@/features/orgs/projects/database/dataGrid/utils/postgresqlConstants';
|
||||||
import type { DataGridProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
|
import type { DataGridProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
|
||||||
import { DataGrid } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
|
import { DataGrid } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
|
||||||
import { DataGridBooleanCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridBooleanCell';
|
import { DataGridBooleanCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridBooleanCell';
|
||||||
import { DataGridDateCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDateCell';
|
import { DataGridDateCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDateCell';
|
||||||
import { DataGridDecimalCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDecimalCell';
|
import { DataGridNumericCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridNumericCell';
|
||||||
import { DataGridIntegerCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridIntegerCell';
|
|
||||||
import { DataGridTextCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridTextCell';
|
import { DataGridTextCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridTextCell';
|
||||||
|
import { isNotEmptyValue } from '@/lib/utils';
|
||||||
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { KeyRound } from 'lucide-react';
|
import { KeyRound } from 'lucide-react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
@@ -68,6 +68,7 @@ export function createDataGridColumn(
|
|||||||
isEditable,
|
isEditable,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
specificType: column.full_data_type,
|
specificType: column.full_data_type,
|
||||||
|
dataType: column.data_type,
|
||||||
maxLength: column.character_maximum_length,
|
maxLength: column.character_maximum_length,
|
||||||
Cell: DataGridTextCell,
|
Cell: DataGridTextCell,
|
||||||
isPrimary: column.is_primary,
|
isPrimary: column.is_primary,
|
||||||
@@ -82,21 +83,13 @@ export function createDataGridColumn(
|
|||||||
foreignKeyRelation: column.foreign_key_relation,
|
foreignKeyRelation: column.foreign_key_relation,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (POSTGRESQL_INTEGER_TYPES.includes(column.data_type)) {
|
if (POSTGRESQL_NUMERIC_TYPES.includes(column.data_type)) {
|
||||||
return {
|
return {
|
||||||
...defaultColumnConfiguration,
|
...defaultColumnConfiguration,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
isCopiable: true,
|
||||||
width: 250,
|
width: 250,
|
||||||
Cell: DataGridIntegerCell,
|
Cell: DataGridNumericCell,
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (POSTGRESQL_DECIMAL_TYPES.includes(column.data_type)) {
|
|
||||||
return {
|
|
||||||
...defaultColumnConfiguration,
|
|
||||||
type: 'text',
|
|
||||||
width: 250,
|
|
||||||
Cell: DataGridDecimalCell,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +130,7 @@ export function createDataGridColumn(
|
|||||||
...defaultColumnConfiguration,
|
...defaultColumnConfiguration,
|
||||||
type: 'date',
|
type: 'date',
|
||||||
width: 200,
|
width: 200,
|
||||||
|
isCopiable: true,
|
||||||
Cell: DataGridDateCell,
|
Cell: DataGridDateCell,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -166,8 +160,12 @@ export default function DataBrowserGrid({
|
|||||||
|
|
||||||
const { mutateAsync: updateRow } = useUpdateRecordWithToastMutation();
|
const { mutateAsync: updateRow } = useUpdateRecordWithToastMutation();
|
||||||
|
|
||||||
|
const sortByString = isNotEmptyValue(sortBy?.[0])
|
||||||
|
? `${sortBy[0].id}.${sortBy[0].desc}`
|
||||||
|
: 'default-order';
|
||||||
|
|
||||||
const { data, status, error, refetch } = useTableQuery(
|
const { data, status, error, refetch } = useTableQuery(
|
||||||
[currentTablePath, limit, currentOffset, sortBy],
|
[currentTablePath, currentOffset, sortByString],
|
||||||
{
|
{
|
||||||
limit,
|
limit,
|
||||||
offset: currentOffset * limit,
|
offset: currentOffset * limit,
|
||||||
@@ -274,6 +272,8 @@ export default function DataBrowserGrid({
|
|||||||
// TODO: Create proper typings for data browser columns
|
// TODO: Create proper typings for data browser columns
|
||||||
columns={memoizedColumns as unknown as DataBrowserGridColumn[]}
|
columns={memoizedColumns as unknown as DataBrowserGridColumn[]}
|
||||||
onSubmit={refetch}
|
onSubmit={refetch}
|
||||||
|
currentOffset={currentOffset}
|
||||||
|
sortByString={sortByString}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
import { Badge } from '@/components/ui/v3/badge';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
import { DataGridCustomizerControls } from '@/features/orgs/projects/common/components/DataGridCustomizerControls';
|
||||||
import { Chip } from '@/components/ui/v2/Chip';
|
|
||||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
|
||||||
import { useDeleteRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteRecordMutation';
|
import { useDeleteRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteRecordMutation';
|
||||||
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||||
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||||
import type { DataGridPaginationProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
|
import type { DataGridPaginationProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
|
||||||
import { DataGridPagination } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
|
import { DataGridPagination } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { triggerToast } from '@/utils/toast';
|
import { triggerToast } from '@/utils/toast';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Row } from 'react-table';
|
import type { Row } from 'react-table';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export interface DataBrowserGridControlsProps extends BoxProps {
|
export interface DataBrowserGridControlsProps {
|
||||||
/**
|
/**
|
||||||
* Props passed to the pagination component.
|
* Props passed to the pagination component.
|
||||||
*/
|
*/
|
||||||
@@ -33,11 +33,9 @@ export interface DataBrowserGridControlsProps extends BoxProps {
|
|||||||
// TODO: Get rid of Data Browser related code from here. This component should
|
// TODO: Get rid of Data Browser related code from here. This component should
|
||||||
// be generic and not depend on Data Browser related data types and logic.
|
// be generic and not depend on Data Browser related data types and logic.
|
||||||
export default function DataBrowserGridControls({
|
export default function DataBrowserGridControls({
|
||||||
className,
|
|
||||||
paginationProps,
|
paginationProps,
|
||||||
refetchData,
|
refetchData,
|
||||||
onInsertRowClick,
|
onInsertRowClick,
|
||||||
...props
|
|
||||||
}: DataBrowserGridControlsProps) {
|
}: DataBrowserGridControlsProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { openAlertDialog } = useDialog();
|
const { openAlertDialog } = useDialog();
|
||||||
@@ -98,28 +96,26 @@ export default function DataBrowserGridControls({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<div className="box sticky top-0 z-20 border-b-1 p-2">
|
||||||
className={twMerge('sticky top-0 z-20 border-b-1 p-2', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={cn(
|
||||||
'mx-auto grid min-h-[38px] grid-flow-col items-center gap-3',
|
'mx-auto grid min-h-10 grid-flow-col items-center gap-3',
|
||||||
numberOfSelectedRows > 0 ? 'justify-between' : 'justify-end',
|
numberOfSelectedRows > 0 ? 'justify-between' : 'justify-end',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{numberOfSelectedRows > 0 && (
|
{numberOfSelectedRows > 0 && (
|
||||||
<div className="grid grid-flow-col place-content-start items-center gap-2">
|
<div className="grid grid-flow-col place-content-start items-center gap-2">
|
||||||
<Chip
|
<Badge
|
||||||
size="small"
|
variant="secondary"
|
||||||
color="info"
|
className="!bg-[#ebf3ff] text-primary dark:!bg-[#1b2534]"
|
||||||
label={`${numberOfSelectedRows} selected`}
|
>
|
||||||
/>
|
{`${numberOfSelectedRows} selected`}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="borderless"
|
variant="outline"
|
||||||
color="error"
|
size="sm"
|
||||||
size="small"
|
className="border-none text-destructive hover:bg-[#f131541a] hover:text-destructive"
|
||||||
loading={status === 'loading'}
|
loading={status === 'loading'}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openAlertDialog({
|
openAlertDialog({
|
||||||
@@ -160,17 +156,13 @@ export default function DataBrowserGridControls({
|
|||||||
{...restPaginationProps}
|
{...restPaginationProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<DataGridCustomizerControls />
|
||||||
<Button
|
<Button onClick={onInsertRowClick} size="sm">
|
||||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
<Plus className="h-4 w-4" /> Insert row
|
||||||
size="small"
|
|
||||||
onClick={onInsertRowClick}
|
|
||||||
>
|
|
||||||
Insert row
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,32 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import { NavLink } from '@/components/common/NavLink';
|
|
||||||
import { FormActivityIndicator } from '@/components/form/FormActivityIndicator';
|
import { FormActivityIndicator } from '@/components/form/FormActivityIndicator';
|
||||||
import { InlineCode } from '@/components/presentational/InlineCode';
|
|
||||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
|
||||||
import { Backdrop } from '@/components/ui/v2/Backdrop';
|
import { Backdrop } from '@/components/ui/v2/Backdrop';
|
||||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
import { Badge } from '@/components/ui/v3/badge';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import { Button } from '@/components/ui/v3/button';
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
import { InlineCode } from '@/components/ui/v3/inline-code';
|
||||||
import { Chip } from '@/components/ui/v2/Chip';
|
import {
|
||||||
import { Divider } from '@/components/ui/v2/Divider';
|
Select,
|
||||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
SelectContent,
|
||||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
SelectItem,
|
||||||
import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon';
|
SelectTrigger,
|
||||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
SelectValue,
|
||||||
import { LockIcon } from '@/components/ui/v2/icons/LockIcon';
|
} from '@/components/ui/v3/select';
|
||||||
import { PencilIcon } from '@/components/ui/v2/icons/PencilIcon';
|
import { Spinner } from '@/components/ui/v3/spinner';
|
||||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
|
||||||
import { TerminalIcon } from '@/components/ui/v2/icons/TerminalIcon';
|
|
||||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
|
||||||
import { UsersIcon } from '@/components/ui/v2/icons/UsersIcon';
|
|
||||||
import { List } from '@/components/ui/v2/List';
|
|
||||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
|
||||||
import { Option } from '@/components/ui/v2/Option';
|
|
||||||
import { Select } from '@/components/ui/v2/Select';
|
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
|
||||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||||
import { useDatabaseQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useDatabaseQuery';
|
import { useDatabaseQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useDatabaseQuery';
|
||||||
import { useDeleteTableWithToastMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteTableMutation';
|
import { useDeleteTableWithToastMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteTableMutation';
|
||||||
import { isSchemaLocked } from '@/features/orgs/projects/database/dataGrid/utils/schemaHelpers';
|
import { isSchemaLocked } from '@/features/orgs/projects/database/dataGrid/utils/schemaHelpers';
|
||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { isEmptyValue, isNotEmptyValue } from '@/lib/utils';
|
import { cn, isEmptyValue, isNotEmptyValue } from '@/lib/utils';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { Info, Lock, Plus, Terminal } from 'lucide-react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import NextLink from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import TableActions from './TableActions';
|
||||||
|
|
||||||
const CreateTableForm = dynamic(
|
const CreateTableForm = dynamic(
|
||||||
() =>
|
() =>
|
||||||
@@ -71,16 +61,17 @@ const EditPermissionsForm = dynamic(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface DataBrowserSidebarProps extends Omit<BoxProps, 'children'> {
|
export interface DataBrowserSidebarProps {
|
||||||
/**
|
className?: string;
|
||||||
* Function to be called when a sidebar item is clicked.
|
}
|
||||||
*/
|
|
||||||
|
export interface DataBrowserSidebarContentProps {
|
||||||
onSidebarItemClick?: (tablePath?: string) => void;
|
onSidebarItemClick?: (tablePath?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataBrowserSidebarContent({
|
function DataBrowserSidebarContent({
|
||||||
onSidebarItemClick,
|
onSidebarItemClick,
|
||||||
}: Pick<DataBrowserSidebarProps, 'onSidebarItemClick'>) {
|
}: DataBrowserSidebarContentProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { openDrawer, openAlertDialog } = useDialog();
|
const { openDrawer, openAlertDialog } = useDialog();
|
||||||
const { project } = useProject();
|
const { project } = useProject();
|
||||||
@@ -136,11 +127,12 @@ function DataBrowserSidebarContent({
|
|||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<Spinner
|
||||||
delay={1000}
|
wrapperClassName="flex-row text-[12px] leading-[1.66] font-normal gap-1"
|
||||||
label="Loading schemas and tables..."
|
className="h-4 w-4 justify-center"
|
||||||
className="justify-center"
|
>
|
||||||
/>
|
Loading schemas and tables...
|
||||||
|
</Spinner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +244,12 @@ function DataBrowserSidebarContent({
|
|||||||
<span className="inline-grid grid-flow-col items-center gap-2">
|
<span className="inline-grid grid-flow-col items-center gap-2">
|
||||||
Permissions
|
Permissions
|
||||||
<InlineCode className="!text-sm+ font-normal">{table}</InlineCode>
|
<InlineCode className="!text-sm+ font-normal">{table}</InlineCode>
|
||||||
<Chip label="Preview" size="small" color="info" component="span" />
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-[#ebf3ff] text-primary dark:bg-[#1b2534]"
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
component: (
|
component: (
|
||||||
@@ -271,59 +268,46 @@ function DataBrowserSidebarContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="flex h-full flex-col justify-between">
|
<div className="flex h-full flex-col justify-between">
|
||||||
<Box className="flex flex-col px-2">
|
<div className="box flex flex-col px-2">
|
||||||
{schemas && schemas.length > 0 && (
|
{schemas && schemas.length > 0 && (
|
||||||
<Select
|
<Select value={selectedSchema} onValueChange={setSelectedSchema}>
|
||||||
renderValue={(option) => (
|
<SelectTrigger className="w-full min-w-[initial] max-w-[220px]">
|
||||||
<span className="grid grid-flow-col items-center gap-1">
|
<SelectValue placeholder="Is null?" />
|
||||||
{option?.label}
|
</SelectTrigger>
|
||||||
</span>
|
<SelectContent>
|
||||||
)}
|
{schemas.map((schema) => (
|
||||||
slotProps={{
|
<SelectItem value={schema.schema_name} key={schema.schema_name}>
|
||||||
listbox: { className: 'max-w-[220px] min-w-[initial] w-full' },
|
<div className="flex items-center gap-2">
|
||||||
popper: { className: 'max-w-[220px] min-w-[initial] w-full' },
|
<p className="text-sm">
|
||||||
}}
|
<span className="text-disabled">schema.</span>
|
||||||
value={selectedSchema}
|
<span className="font-medium">{schema.schema_name}</span>
|
||||||
onChange={(_event, value) => setSelectedSchema(value as string)}
|
</p>
|
||||||
>
|
{(isSchemaLocked(schema.schema_name) ||
|
||||||
{schemas.map((schema) => (
|
isGitHubConnected) && (
|
||||||
<Option
|
<Lock
|
||||||
className="grid grid-flow-col items-center gap-1"
|
className="text-[#556378] dark:text-[#a2b3be]"
|
||||||
value={schema.schema_name}
|
size={12}
|
||||||
key={schema.schema_name}
|
/>
|
||||||
>
|
)}
|
||||||
<Text className="text-sm">
|
</div>
|
||||||
<Text component="span" color="disabled">
|
</SelectItem>
|
||||||
schema.
|
))}
|
||||||
</Text>
|
</SelectContent>
|
||||||
<Text component="span" className="font-medium">
|
|
||||||
{schema.schema_name}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
{(isSchemaLocked(schema.schema_name) || isGitHubConnected) && (
|
|
||||||
<LockIcon
|
|
||||||
className="h-3 w-3"
|
|
||||||
sx={{ color: 'text.secondary' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{isGitHubConnected && (
|
{isGitHubConnected && (
|
||||||
<Box className="mt-1.5 flex items-center gap-1 px-2">
|
<div className="box mt-1.5 flex items-center gap-1 px-2">
|
||||||
<InfoIcon className="h-4 w-4" sx={{ color: 'text.secondary' }} />
|
<Info className="h-4 w-4 text-disabled" />
|
||||||
<Text className="text-xs" color="secondary">
|
<p className="text-xs text-disabled">
|
||||||
GitHub connected - use the CLI for schema changes
|
GitHub connected - use the CLI for schema changes
|
||||||
</Text>
|
</p>
|
||||||
</Box>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isSelectedSchemaLocked && (
|
{!isSelectedSchemaLocked && (
|
||||||
<Button
|
<Button
|
||||||
variant="borderless"
|
variant="link"
|
||||||
endIcon={<PlusIcon />}
|
className="mt-1 flex w-full justify-between px-[0.625rem] !text-sm+ text-primary hover:bg-accent hover:no-underline disabled:text-disabled"
|
||||||
className="mt-1 w-full justify-between px-2"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openDrawer({
|
openDrawer({
|
||||||
title: 'Create a New Table',
|
title: 'Create a New Table',
|
||||||
@@ -335,202 +319,136 @@ function DataBrowserSidebarContent({
|
|||||||
}}
|
}}
|
||||||
disabled={isGitHubConnected}
|
disabled={isGitHubConnected}
|
||||||
>
|
>
|
||||||
New Table
|
New Table <Plus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isNotEmptyValue(schemas) && isEmptyValue(tablesInSelectedSchema) && (
|
{isNotEmptyValue(schemas) && isEmptyValue(tablesInSelectedSchema) && (
|
||||||
<Text className="px-2 py-1.5 text-xs" color="disabled">
|
<p className="px-2 py-1.5 text-xs text-disabled">No tables found.</p>
|
||||||
No tables found.
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
<nav aria-label="Database navigation">
|
<nav aria-label="Database navigation">
|
||||||
{isNotEmptyValue(tablesInSelectedSchema) && (
|
{isNotEmptyValue(tablesInSelectedSchema) && (
|
||||||
<List className="grid gap-1 pb-6">
|
<ul className="w-full max-w-full pb-6">
|
||||||
{tablesInSelectedSchema.map((table) => {
|
{tablesInSelectedSchema.map((table) => {
|
||||||
const tablePath = `${table.table_schema}.${table.table_name}`;
|
const tablePath = `${table.table_schema}.${table.table_name}`;
|
||||||
const isSelected = `${schemaSlug}.${tableSlug}` === tablePath;
|
const isSelected = `${schemaSlug}.${tableSlug}` === tablePath;
|
||||||
const isSidebarMenuOpen = sidebarMenuTable === tablePath;
|
const isSidebarMenuOpen = sidebarMenuTable === tablePath;
|
||||||
return (
|
return (
|
||||||
<ListItem.Root
|
<li className="group pb-1" key={tablePath}>
|
||||||
className="group"
|
<Button
|
||||||
key={tablePath}
|
asChild
|
||||||
secondaryAction={
|
variant="link"
|
||||||
<Dropdown.Root
|
size="sm"
|
||||||
id="table-management-menu"
|
|
||||||
onOpen={() => setSidebarMenuTable(tablePath)}
|
|
||||||
onClose={() => setSidebarMenuTable(undefined)}
|
|
||||||
>
|
|
||||||
<Dropdown.Trigger
|
|
||||||
asChild
|
|
||||||
hideChevron
|
|
||||||
disabled={tablePath === removableTable}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
variant="borderless"
|
|
||||||
color={isSelected ? 'primary' : 'secondary'}
|
|
||||||
className={twMerge(
|
|
||||||
!isSelected &&
|
|
||||||
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DotsHorizontalIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Dropdown.Trigger>
|
|
||||||
<Dropdown.Content
|
|
||||||
menu
|
|
||||||
PaperProps={{ className: 'w-52' }}
|
|
||||||
>
|
|
||||||
{isGitHubConnected ? (
|
|
||||||
<Dropdown.Item
|
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
|
||||||
onClick={() =>
|
|
||||||
handleEditPermissionClick(
|
|
||||||
table.table_schema,
|
|
||||||
table.table_name,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<UsersIcon
|
|
||||||
className="h-4 w-4"
|
|
||||||
sx={{ color: 'text.secondary' }}
|
|
||||||
/>
|
|
||||||
<span>View Permissions</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
) : (
|
|
||||||
[
|
|
||||||
!isSelectedSchemaLocked && (
|
|
||||||
<Dropdown.Item
|
|
||||||
key="edit-table"
|
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
|
||||||
onClick={() =>
|
|
||||||
openDrawer({
|
|
||||||
title: 'Edit Table',
|
|
||||||
component: (
|
|
||||||
<EditTableForm
|
|
||||||
onSubmit={async (tableName) => {
|
|
||||||
await queryClient.refetchQueries([
|
|
||||||
`${dataSourceSlug}.${table.table_schema}.${tableName}`,
|
|
||||||
]);
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
schema={table.table_schema}
|
|
||||||
table={table}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PencilIcon
|
|
||||||
className="h-4 w-4"
|
|
||||||
sx={{ color: 'text.secondary' }}
|
|
||||||
/>
|
|
||||||
<span>Edit Table</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
),
|
|
||||||
!isSelectedSchemaLocked && (
|
|
||||||
<Divider
|
|
||||||
key="edit-table-separator"
|
|
||||||
component="li"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
<Dropdown.Item
|
|
||||||
key="edit-permissions"
|
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
|
||||||
onClick={() =>
|
|
||||||
handleEditPermissionClick(
|
|
||||||
table.table_schema,
|
|
||||||
table.table_name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<UsersIcon
|
|
||||||
className="h-4 w-4"
|
|
||||||
sx={{ color: 'text.secondary' }}
|
|
||||||
/>
|
|
||||||
<span>Edit Permissions</span>
|
|
||||||
</Dropdown.Item>,
|
|
||||||
!isSelectedSchemaLocked && (
|
|
||||||
<Divider
|
|
||||||
key="edit-permissions-separator"
|
|
||||||
component="li"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
!isSelectedSchemaLocked && (
|
|
||||||
<Dropdown.Item
|
|
||||||
key="delete-table"
|
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
|
||||||
sx={{ color: 'error.main' }}
|
|
||||||
onClick={() =>
|
|
||||||
handleDeleteTableClick(
|
|
||||||
table.table_schema,
|
|
||||||
table.table_name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TrashIcon
|
|
||||||
className="h-4 w-4"
|
|
||||||
sx={{ color: 'error.main' }}
|
|
||||||
/>
|
|
||||||
<span>Delete Table</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
</Dropdown.Content>
|
|
||||||
</Dropdown.Root>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListItem.Button
|
|
||||||
dense
|
|
||||||
selected={isSelected}
|
|
||||||
disabled={tablePath === removableTable}
|
disabled={tablePath === removableTable}
|
||||||
className="group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
|
className={cn(
|
||||||
sx={{
|
'flex w-full max-w-full justify-between pl-0 text-sm+ hover:bg-accent hover:no-underline',
|
||||||
paddingRight:
|
{
|
||||||
(isSelected || isSidebarMenuOpen) &&
|
'bg-table-selected': isSelected,
|
||||||
'2.25rem !important',
|
},
|
||||||
}}
|
)}
|
||||||
component={NavLink}
|
|
||||||
href={`/orgs/${orgSlug}/projects/${appSubdomain}/database/browser/default/${table.table_schema}/${table.table_name}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (onSidebarItemClick) {
|
|
||||||
onSidebarItemClick(`default.${tablePath}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ListItem.Text>{table.table_name}</ListItem.Text>
|
<div>
|
||||||
</ListItem.Button>
|
<NextLink
|
||||||
</ListItem.Root>
|
className={cn(
|
||||||
|
'flex h-full w-[calc(100%-1.6rem)] items-center p-[0.625rem] pr-0 text-left',
|
||||||
|
{
|
||||||
|
'text-primary-main': isSelected,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (onSidebarItemClick) {
|
||||||
|
onSidebarItemClick(`default.${tablePath}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
href={`/orgs/${orgSlug}/projects/${appSubdomain}/database/browser/default/${table.table_schema}/${table.table_name}`}
|
||||||
|
>
|
||||||
|
<span className="!truncate text-ellipsis">
|
||||||
|
{table.table_name}
|
||||||
|
</span>
|
||||||
|
</NextLink>
|
||||||
|
<TableActions
|
||||||
|
tableName={table.table_name}
|
||||||
|
disabled={tablePath === removableTable}
|
||||||
|
open={isSidebarMenuOpen}
|
||||||
|
onOpen={() => setSidebarMenuTable(tablePath)}
|
||||||
|
onClose={() => setSidebarMenuTable(undefined)}
|
||||||
|
className={cn(
|
||||||
|
'relative z-10 opacity-0 group-hover:opacity-100',
|
||||||
|
{
|
||||||
|
'opacity-100': isSelected || isSidebarMenuOpen,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
isSelectedNotSchemaLocked={!isSelectedSchemaLocked}
|
||||||
|
onViewPermissions={() =>
|
||||||
|
handleEditPermissionClick(
|
||||||
|
table.table_schema,
|
||||||
|
table.table_name,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onEditTable={() =>
|
||||||
|
openDrawer({
|
||||||
|
title: 'Edit Table',
|
||||||
|
component: (
|
||||||
|
<EditTableForm
|
||||||
|
onSubmit={async (tableName) => {
|
||||||
|
await queryClient.refetchQueries([
|
||||||
|
`${dataSourceSlug}.${table.table_schema}.${tableName}`,
|
||||||
|
]);
|
||||||
|
await refetch();
|
||||||
|
}}
|
||||||
|
schema={table.table_schema}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onEditPermissions={() =>
|
||||||
|
handleEditPermissionClick(
|
||||||
|
table.table_schema,
|
||||||
|
table.table_name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onDelete={() =>
|
||||||
|
handleDeleteTableClick(
|
||||||
|
table.table_schema,
|
||||||
|
table.table_name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</List>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</Box>
|
</div>
|
||||||
|
|
||||||
<Box className="border-t">
|
<div className="box border-t">
|
||||||
<ListItem.Button
|
<Button
|
||||||
dense
|
size="sm"
|
||||||
selected={asPath === sqlEditorHref}
|
variant="link"
|
||||||
className="flex border group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
|
asChild
|
||||||
component={NavLink}
|
className={cn(
|
||||||
href={sqlEditorHref}
|
'flex rounded-none border text-sm+ hover:bg-accent hover:no-underline group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9',
|
||||||
|
{ 'bg-table-selected text-primary-main': asPath === sqlEditorHref },
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full flex-row items-center justify-center space-x-4">
|
<NextLink href={sqlEditorHref}>
|
||||||
<TerminalIcon />
|
<div className="flex w-full flex-row items-center justify-center space-x-4">
|
||||||
<span className="flex">SQL Editor</span>
|
<Terminal />
|
||||||
</div>
|
<span className="flex">SQL Editor</span>
|
||||||
</ListItem.Button>
|
</div>
|
||||||
</Box>
|
</NextLink>
|
||||||
</Box>
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DataBrowserSidebar({
|
export default function DataBrowserSidebar({
|
||||||
className,
|
className,
|
||||||
onSidebarItemClick,
|
|
||||||
...props
|
|
||||||
}: DataBrowserSidebarProps) {
|
}: DataBrowserSidebarProps) {
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { project } = useProject();
|
const { project } = useProject();
|
||||||
@@ -541,11 +459,7 @@ export default function DataBrowserSidebar({
|
|||||||
setExpanded(!expanded);
|
setExpanded(!expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSidebarItemClick(tablePath?: string) {
|
function handleSidebarItemClick() {
|
||||||
if (onSidebarItemClick && tablePath) {
|
|
||||||
onSidebarItemClick(tablePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
setExpanded(false);
|
setExpanded(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,24 +500,24 @@ export default function DataBrowserSidebar({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box
|
<aside
|
||||||
component="aside"
|
className={cn(
|
||||||
className={twMerge(
|
'box absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 pb-17 pt-2 motion-safe:transition-transform sm:relative sm:z-0 sm:h-full sm:pb-0 sm:pt-2.5 sm:transition-none',
|
||||||
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 pb-17 pt-2 motion-safe:transition-transform sm:relative sm:z-0 sm:h-full sm:pb-0 sm:pt-2.5 sm:transition-none',
|
|
||||||
expanded ? 'translate-x-0' : '-translate-x-full sm:translate-x-0',
|
expanded ? 'translate-x-0' : '-translate-x-full sm:translate-x-0',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<RetryableErrorBoundary>
|
<RetryableErrorBoundary>
|
||||||
<DataBrowserSidebarContent
|
<DataBrowserSidebarContent
|
||||||
onSidebarItemClick={handleSidebarItemClick}
|
onSidebarItemClick={handleSidebarItemClick}
|
||||||
/>
|
/>
|
||||||
</RetryableErrorBoundary>
|
</RetryableErrorBoundary>
|
||||||
</Box>
|
</aside>
|
||||||
|
|
||||||
<IconButton
|
<Button
|
||||||
className="absolute bottom-4 left-8 z-[38] h-11 w-11 rounded-full md:hidden"
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="absolute bottom-4 left-8 z-[38] h-11 w-11 rounded-full bg-primary md:hidden"
|
||||||
onClick={toggleExpanded}
|
onClick={toggleExpanded}
|
||||||
aria-label="Toggle sidebar"
|
aria-label="Toggle sidebar"
|
||||||
>
|
>
|
||||||
@@ -613,7 +527,7 @@ export default function DataBrowserSidebar({
|
|||||||
src="/assets/table.svg"
|
src="/assets/table.svg"
|
||||||
alt="A monochrome table"
|
alt="A monochrome table"
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Button } from '@/components/ui/v3/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/v3/dropdown-menu';
|
||||||
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Ellipsis, SquarePen, Trash2, Users } from 'lucide-react';
|
||||||
|
|
||||||
|
const menuItemClassName =
|
||||||
|
'flex hover:cursor-pointer hover:bg-data-cell-bg h-9 font-medium items-center justify-start gap-2 rounded-none border border-b-1 text-sm+ leading-4';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tableName: string;
|
||||||
|
open: boolean;
|
||||||
|
className?: string;
|
||||||
|
onOpen: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
disabled: boolean;
|
||||||
|
isSelectedNotSchemaLocked: boolean;
|
||||||
|
onDelete: () => void;
|
||||||
|
onEditPermissions: () => void;
|
||||||
|
onViewPermissions: () => void;
|
||||||
|
onEditTable: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TableActions({
|
||||||
|
tableName,
|
||||||
|
open,
|
||||||
|
className,
|
||||||
|
onClose,
|
||||||
|
onOpen,
|
||||||
|
disabled,
|
||||||
|
isSelectedNotSchemaLocked,
|
||||||
|
onDelete,
|
||||||
|
onEditPermissions,
|
||||||
|
onViewPermissions,
|
||||||
|
onEditTable,
|
||||||
|
}: Props) {
|
||||||
|
const { project } = useProject();
|
||||||
|
const isGitHubConnected = !!project?.githubRepository;
|
||||||
|
|
||||||
|
function handleOnOpenChange(newOpenState: boolean) {
|
||||||
|
if (newOpenState) {
|
||||||
|
onOpen();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={open} onOpenChange={handleOnOpenChange}>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
className={cn(className)}
|
||||||
|
disabled={disabled}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id={`table-management-menu-${tableName}`}
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 border-none bg-transparent px-0 hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<Ellipsis />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="bottom" align="start" className="w-52 p-0">
|
||||||
|
{isGitHubConnected ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={menuItemClassName}
|
||||||
|
onClick={onViewPermissions}
|
||||||
|
>
|
||||||
|
<Users className="h-4 w-4" /> <span>View Permissions</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isSelectedNotSchemaLocked && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={menuItemClassName}
|
||||||
|
onClick={onEditTable}
|
||||||
|
>
|
||||||
|
<SquarePen className="h-4 w-4" /> <span>Edit Table</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={menuItemClassName}
|
||||||
|
onClick={onEditPermissions}
|
||||||
|
>
|
||||||
|
<Users className="h-4 w-4" /> <span>Edit Permissions</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isSelectedNotSchemaLocked && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={cn(
|
||||||
|
menuItemClassName,
|
||||||
|
'!text-sm+ font-medium !text-destructive',
|
||||||
|
)}
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
<span>Delete Table</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableActions;
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import { InlineCode } from '@/components/presentational/InlineCode';
|
import { FormInput } from '@/components/form/FormInput';
|
||||||
|
import { FormSelect } from '@/components/form/FormSelect';
|
||||||
|
import { FormTextarea } from '@/components/form/FormTextarea';
|
||||||
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
|
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
|
||||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
import { InlineCode } from '@/components/ui/v3/inline-code';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import { SelectItem } from '@/components/ui/v3/select';
|
||||||
import { KeyIcon } from '@/components/ui/v2/icons/KeyIcon';
|
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
|
||||||
import { Option } from '@/components/ui/v2/Option';
|
|
||||||
import { Select } from '@/components/ui/v2/Select';
|
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
|
||||||
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||||
import { getInputType } from '@/features/orgs/projects/database/dataGrid/utils/inputHelpers';
|
import { getInputType } from '@/features/orgs/projects/database/dataGrid/utils/inputHelpers';
|
||||||
import { normalizeDefaultValue } from '@/features/orgs/projects/database/dataGrid/utils/normalizeDefaultValue';
|
import { normalizeDefaultValue } from '@/features/orgs/projects/database/dataGrid/utils/normalizeDefaultValue';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { cn } from '@/lib/utils';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { KeyRound } from 'lucide-react';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
export interface DatabaseRecordInputGroupProps extends BoxProps {
|
export interface DatabaseRecordInputGroupProps {
|
||||||
/**
|
/**
|
||||||
* List of columns for which input fields should be generated.
|
* List of columns for which input fields should be generated.
|
||||||
*/
|
*/
|
||||||
@@ -30,6 +29,25 @@ export interface DatabaseRecordInputGroupProps extends BoxProps {
|
|||||||
* Determines whether the first input field should be focused.
|
* Determines whether the first input field should be focused.
|
||||||
*/
|
*/
|
||||||
autoFocusFirstInput?: boolean;
|
autoFocusFirstInput?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBooleanValueTransformer(isNullable: boolean) {
|
||||||
|
return function transformBooleanValue(value: string | null) {
|
||||||
|
let convertedValue = value;
|
||||||
|
|
||||||
|
if (convertedValue === null) {
|
||||||
|
convertedValue = isNullable ? 'null' : '';
|
||||||
|
} else if (convertedValue === 'null' || convertedValue === '') {
|
||||||
|
convertedValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertedValue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertNullToEmptyString(value: string | null) {
|
||||||
|
return value === null ? '' : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlaceholder(
|
function getPlaceholder(
|
||||||
@@ -71,28 +89,30 @@ export default function DatabaseRecordInputGroup({
|
|||||||
columns,
|
columns,
|
||||||
autoFocusFirstInput,
|
autoFocusFirstInput,
|
||||||
className,
|
className,
|
||||||
...props
|
|
||||||
}: DatabaseRecordInputGroupProps) {
|
}: DatabaseRecordInputGroupProps) {
|
||||||
const {
|
const { control } = useFormContext();
|
||||||
control,
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
||||||
register,
|
|
||||||
formState: { errors },
|
useEffect(() => {
|
||||||
} = useFormContext();
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function getRef(index: number) {
|
||||||
|
return (element: HTMLTextAreaElement | HTMLInputElement | null) => {
|
||||||
|
if (element && index === 0 && autoFocusFirstInput) {
|
||||||
|
inputRef.current = element;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box component="section" className={twMerge('py-3', className)} {...props}>
|
<section className={cn('box py-3 font-display', className)}>
|
||||||
{title && (
|
{title && <h2 className="mb-1.5 mt-3 text-sm+ font-bold">{title}</h2>}
|
||||||
<Text variant="h2" className="mb-1.5 mt-3 text-sm+ font-bold">
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{description && (
|
{description && (
|
||||||
<Text className="mb-3 text-xs" color="secondary">
|
<p className="mb-3 text-xs text-secondary">{description}</p>
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{columns.map(
|
{columns.map(
|
||||||
(
|
(
|
||||||
@@ -122,99 +142,77 @@ export default function DatabaseRecordInputGroup({
|
|||||||
isNullable,
|
isNullable,
|
||||||
);
|
);
|
||||||
|
|
||||||
const InputLabel = (
|
const inputLabel = (
|
||||||
<span className="inline-grid grid-flow-col gap-1">
|
<span className="inline-grid grid-flow-col gap-1">
|
||||||
<span className="inline-grid grid-flow-col items-center gap-1">
|
<span className="inline-grid grid-flow-col items-center gap-1 break-all">
|
||||||
{isPrimary && <KeyIcon className="text-base text-inherit" />}
|
{isPrimary && (
|
||||||
|
<KeyRound className="text-base text-inherit" size={13} />
|
||||||
|
)}
|
||||||
<span>{columnId}</span>
|
<span>{columnId}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<InlineCode className="h-[18px]">
|
<InlineCode
|
||||||
|
className="h-[1.125rem] overflow-hidden whitespace-nowrap leading-[1.125rem]"
|
||||||
|
style={{ textOverflow: 'clip' }}
|
||||||
|
title={specificType}
|
||||||
|
>
|
||||||
{specificType}
|
{specificType}
|
||||||
{maxLength ? `(${maxLength})` : null}
|
{maxLength ? `(${maxLength})` : null}
|
||||||
</InlineCode>
|
</InlineCode>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
const commonFormControlProps = {
|
|
||||||
label: InputLabel,
|
|
||||||
error: Boolean(errors[columnId]),
|
|
||||||
helperText:
|
|
||||||
comment ||
|
|
||||||
(typeof errors[columnId]?.message === 'string'
|
|
||||||
? (errors[columnId]?.message as string)
|
|
||||||
: null),
|
|
||||||
hideEmptyHelperText: true,
|
|
||||||
fullWidth: true,
|
|
||||||
className: 'py-3',
|
|
||||||
};
|
|
||||||
|
|
||||||
const commonLabelProps = {
|
|
||||||
className: 'grid grid-flow-row justify-items-start gap-1',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type === 'boolean') {
|
if (type === 'boolean') {
|
||||||
return (
|
return (
|
||||||
<Controller
|
<FormSelect
|
||||||
|
key={columnId}
|
||||||
|
inline
|
||||||
name={columnId}
|
name={columnId}
|
||||||
control={control}
|
control={control}
|
||||||
key={columnId}
|
label={inputLabel}
|
||||||
render={({ field }) => (
|
placeholder="Select an option"
|
||||||
<Select
|
helperText={comment}
|
||||||
{...commonFormControlProps}
|
transformValue={getBooleanValueTransformer(!!isNullable)}
|
||||||
{...field}
|
>
|
||||||
onChange={(_event, value) => field.onChange(value)}
|
<SelectItem value="true">
|
||||||
variant="inline"
|
<ReadOnlyToggle checked />
|
||||||
id={columnId}
|
</SelectItem>
|
||||||
value={field.value || 'null'}
|
|
||||||
placeholder="Select an option"
|
|
||||||
className={twMerge(
|
|
||||||
!field.value && 'text-sm font-normal',
|
|
||||||
'py-3',
|
|
||||||
)}
|
|
||||||
autoFocus={index === 0 && autoFocusFirstInput}
|
|
||||||
slotProps={{ label: commonLabelProps }}
|
|
||||||
>
|
|
||||||
<Option value="true">
|
|
||||||
<ReadOnlyToggle checked />
|
|
||||||
</Option>
|
|
||||||
|
|
||||||
<Option value="false">
|
<SelectItem value="false">
|
||||||
<ReadOnlyToggle checked={false} />
|
<ReadOnlyToggle checked={false} />
|
||||||
</Option>
|
</SelectItem>
|
||||||
|
|
||||||
{isNullable && (
|
{isNullable && (
|
||||||
<Option value="null">
|
<SelectItem value="null">
|
||||||
<ReadOnlyToggle checked={null} />
|
<ReadOnlyToggle checked={null} />
|
||||||
</Option>
|
</SelectItem>
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</FormSelect>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const InputComponent = isMultiline ? FormTextarea : FormInput;
|
||||||
return (
|
return (
|
||||||
<Input
|
<InputComponent
|
||||||
{...commonFormControlProps}
|
ref={getRef(index)}
|
||||||
{...register(columnId)}
|
|
||||||
variant="inline"
|
|
||||||
id={columnId}
|
|
||||||
key={columnId}
|
key={columnId}
|
||||||
type={getInputType({ type, specificType })}
|
inline
|
||||||
|
name={columnId}
|
||||||
|
control={control}
|
||||||
|
label={inputLabel}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
multiline={isMultiline}
|
helperText={comment}
|
||||||
rows={5}
|
transformValue={convertNullToEmptyString}
|
||||||
autoFocus={index === 0 && autoFocusFirstInput}
|
className={cn(
|
||||||
slotProps={{
|
{ 'resize-none': isMultiline },
|
||||||
label: commonLabelProps,
|
'focus-visible:ring-0',
|
||||||
}}
|
)}
|
||||||
|
type={getInputType({ type, specificType })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Text } from '@/components/ui/v2/Text';
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -70,9 +69,9 @@ export default function RuleGroupControls({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
) : (
|
) : (
|
||||||
<Text className="p-2 !font-medium">
|
<p className="p-2 !font-medium">
|
||||||
{operatorDictionary[currentOperator]}
|
{operatorDictionary[currentOperator]}
|
||||||
</Text>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function RuleValueInput({
|
|||||||
if (operator === '_is_null') {
|
if (operator === '_is_null') {
|
||||||
const defaultValue = comboboxValue ?? undefined;
|
const defaultValue = comboboxValue ?? undefined;
|
||||||
const triggerClasses =
|
const triggerClasses =
|
||||||
'border hover:bg-accent hover:text-accent-foreground focus:ring-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
|
'border hover:bg-accent-background hover:text-accent-foreground focus:ring-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||||
|
import { showLoadingToast } from '@/utils/toast';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import type { UseUpdateRecordMutationOptions } from './useUpdateRecordMutation';
|
import type { UseUpdateRecordMutationOptions } from './useUpdateRecordMutation';
|
||||||
@@ -24,6 +25,7 @@ export default function useUpdateRecordWithToastMutation(
|
|||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
const loadingToastId = showLoadingToast('Saving data...', {
|
const loadingToastId = showLoadingToast('Saving data...', {
|
||||||
id: 'data-browser-data-save',
|
id: 'data-browser-data-save',
|
||||||
|
...getToastStyleProps(),
|
||||||
});
|
});
|
||||||
|
|
||||||
setToastId(loadingToastId);
|
setToastId(loadingToastId);
|
||||||
@@ -35,9 +37,13 @@ export default function useUpdateRecordWithToastMutation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'success' && toastId) {
|
if (status === 'success' && toastId) {
|
||||||
toast.remove(toastId);
|
setTimeout(() => {
|
||||||
|
toast.remove(toastId);
|
||||||
triggerToast('Your changes were successfully saved.');
|
toast.success(
|
||||||
|
'Your changes were successfully saved.',
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
}, [status, toastId]);
|
}, [status, toastId]);
|
||||||
|
|
||||||
|
|||||||
@@ -480,6 +480,7 @@ export interface DataBrowserGridColumn<TData extends object = {}>
|
|||||||
* Determines whether or not the cell content is copiable.
|
* Determines whether or not the cell content is copiable.
|
||||||
*/
|
*/
|
||||||
isCopiable?: boolean;
|
isCopiable?: boolean;
|
||||||
|
dataType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const POSTGRESQL_ERROR_CODES = {
|
|||||||
*
|
*
|
||||||
* @docs https://www.postgresql.org/docs/current/datatype-numeric.html
|
* @docs https://www.postgresql.org/docs/current/datatype-numeric.html
|
||||||
*/
|
*/
|
||||||
export const POSTGRESQL_INTEGER_TYPES = [
|
export const POSTGRESQL_NUMERIC_TYPES = [
|
||||||
'smallint',
|
'smallint',
|
||||||
'integer',
|
'integer',
|
||||||
'bigint',
|
'bigint',
|
||||||
@@ -27,16 +27,21 @@ export const POSTGRESQL_INTEGER_TYPES = [
|
|||||||
'serial',
|
'serial',
|
||||||
'bigserial',
|
'bigserial',
|
||||||
'oid',
|
'oid',
|
||||||
|
'numeric',
|
||||||
|
'real',
|
||||||
|
'double precision',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const POSTGRESQL_DECIMAL_TYPES = ['numeric', 'real', 'double precision'];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Character data types in PostgreSQL.
|
* Character data types in PostgreSQL.
|
||||||
*
|
*
|
||||||
* @docs https://www.postgresql.org/docs/current/datatype-character.html
|
* @docs https://www.postgresql.org/docs/current/datatype-character.html
|
||||||
*/
|
*/
|
||||||
export const POSTGRESQL_CHARACTER_TYPES = ['varchar', 'character', 'text'];
|
export const POSTGRESQL_CHARACTER_TYPES = [
|
||||||
|
'character varying',
|
||||||
|
'character',
|
||||||
|
'text',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON data types in PostgreSQL.
|
* JSON data types in PostgreSQL.
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export function createDynamicValidationSchema(
|
|||||||
[column.id]: createUUIDValidationSchema(details),
|
[column.id]: createUUIDValidationSchema(details),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
column.type === 'date' &&
|
column.type === 'date' &&
|
||||||
['time', 'timetz', 'interval'].includes(column.specificType as string)
|
['time', 'timetz', 'interval'].includes(column.specificType as string)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ErrorMessage } from '@/components/presentational/ErrorMessage';
|
||||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import { Avatar } from '@/components/ui/v2/Avatar';
|
import { Avatar } from '@/components/ui/v2/Avatar';
|
||||||
@@ -11,14 +12,33 @@ import { Link } from '@/components/ui/v2/Link';
|
|||||||
import { List } from '@/components/ui/v2/List';
|
import { List } from '@/components/ui/v2/List';
|
||||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
import { Text } from '@/components/ui/v2/Text';
|
||||||
|
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
|
||||||
|
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
||||||
import { EditRepositorySettings } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
|
import { EditRepositorySettings } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
|
||||||
import { useGetGithubRepositoriesQuery } from '@/generated/graphql';
|
import {
|
||||||
|
getGitHubToken,
|
||||||
|
saveGitHubToken,
|
||||||
|
} from '@/features/orgs/projects/git/common/utils';
|
||||||
|
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||||
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
|
import { useGetAuthUserProvidersQuery } from '@/generated/graphql';
|
||||||
|
import { useAccessToken } from '@/hooks/useAccessToken';
|
||||||
|
import { GitHubAPIError, listGitHubInstallationRepos } from '@/lib/github';
|
||||||
|
import { isEmptyValue } from '@/lib/utils';
|
||||||
|
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||||
|
import { nhost } from '@/utils/nhost';
|
||||||
import { Divider } from '@mui/material';
|
import { Divider } from '@mui/material';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
import NavLink from 'next/link';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export type ConnectGitHubModalState = 'CONNECTING' | 'EDITING';
|
export type ConnectGitHubModalState =
|
||||||
|
| 'CONNECTING'
|
||||||
|
| 'EDITING'
|
||||||
|
| 'EXPIRED_GITHUB_SESSION'
|
||||||
|
| 'GITHUB_CONNECTION_REQUIRED';
|
||||||
|
|
||||||
export interface ConnectGitHubModalProps {
|
export interface ConnectGitHubModalProps {
|
||||||
/**
|
/**
|
||||||
@@ -28,18 +48,153 @@ export interface ConnectGitHubModalProps {
|
|||||||
close?: VoidFunction;
|
close?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GitHubData {
|
||||||
|
githubAppInstallations: Array<{
|
||||||
|
id: number;
|
||||||
|
accountLogin?: string;
|
||||||
|
accountAvatarUrl?: string;
|
||||||
|
}>;
|
||||||
|
githubRepositories: Array<{
|
||||||
|
id: number;
|
||||||
|
node_id: string;
|
||||||
|
name: string;
|
||||||
|
fullName: string;
|
||||||
|
githubAppInstallation: {
|
||||||
|
accountLogin?: string;
|
||||||
|
accountAvatarUrl?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
const [ConnectGitHubModalState, setConnectGitHubModalState] =
|
const [ConnectGitHubModalState, setConnectGitHubModalState] =
|
||||||
useState<ConnectGitHubModalState>('CONNECTING');
|
useState<ConnectGitHubModalState>('CONNECTING');
|
||||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
|
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
|
||||||
|
const [githubData, setGithubData] = useState<GitHubData | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { project, loading: loadingProject } = useProject();
|
||||||
|
const { org, loading: loadingOrg } = useCurrentOrg();
|
||||||
|
const hostname = useHostName();
|
||||||
|
const token = useAccessToken();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading: loadingGithubConnected,
|
||||||
|
error: errorGithubConnected,
|
||||||
|
} = useGetAuthUserProvidersQuery();
|
||||||
|
|
||||||
const { data, loading, error, startPolling } =
|
const githubProvider = data?.authUserProviders?.find(
|
||||||
useGetGithubRepositoriesQuery();
|
(item) => item.providerId === 'github',
|
||||||
|
);
|
||||||
|
|
||||||
|
const getGitHubConnectUrl = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return nhost.auth.signInProviderURL('github', {
|
||||||
|
connect: token,
|
||||||
|
redirectTo: `${window.location.origin}?signinProvider=github&state=signin-refresh:${org.slug}:${project?.subdomain}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
startPolling(2000);
|
if (loadingGithubConnected) {
|
||||||
}, [startPolling]);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchGitHubData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (isEmptyValue(githubProvider)) {
|
||||||
|
setConnectGitHubModalState('GITHUB_CONNECTION_REQUIRED');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const githubToken = getGitHubToken();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!githubToken?.authUserProviderId ||
|
||||||
|
githubProvider!.id !== githubToken.authUserProviderId
|
||||||
|
) {
|
||||||
|
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { refreshToken, expiresAt: expiresAtString } = githubToken;
|
||||||
|
let accessToken = githubToken?.accessToken;
|
||||||
|
|
||||||
|
const expiresAt = new Date(expiresAtString).getTime();
|
||||||
|
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const expiresAtMargin = 60 * 1000;
|
||||||
|
if (expiresAt - currentTime < expiresAtMargin) {
|
||||||
|
if (!refreshToken) {
|
||||||
|
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshResponse = await nhost.auth.refreshProviderToken(
|
||||||
|
'github',
|
||||||
|
{ refreshToken },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!refreshResponse.body) {
|
||||||
|
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveGitHubToken({
|
||||||
|
...refreshResponse.body,
|
||||||
|
authUserProviderId: githubProvider!.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
accessToken = refreshResponse.body.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installations = await listGitHubInstallationRepos(accessToken);
|
||||||
|
|
||||||
|
const transformedData = {
|
||||||
|
githubAppInstallations: installations.map((item) => ({
|
||||||
|
id: item.installation.id,
|
||||||
|
accountLogin: item.installation.account?.login,
|
||||||
|
accountAvatarUrl: item.installation.account?.avatar_url,
|
||||||
|
})),
|
||||||
|
githubRepositories: installations.flatMap((item) =>
|
||||||
|
item.repositories.map((repo) => ({
|
||||||
|
id: repo.id,
|
||||||
|
node_id: repo.node_id,
|
||||||
|
name: repo.name,
|
||||||
|
fullName: repo.full_name,
|
||||||
|
githubAppInstallation: {
|
||||||
|
accountLogin: item.installation.account?.login,
|
||||||
|
accountAvatarUrl: item.installation.account?.avatar_url,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
setGithubData(transformedData);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching GitHub data:', err);
|
||||||
|
if (err instanceof GitHubAPIError && err.status === 401) {
|
||||||
|
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(err?.message, getToastStyleProps());
|
||||||
|
close?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchGitHubData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [githubProvider, loadingGithubConnected]);
|
||||||
|
|
||||||
const handleSelectAnotherRepository = () => {
|
const handleSelectAnotherRepository = () => {
|
||||||
setSelectedRepoId(null);
|
setSelectedRepoId(null);
|
||||||
@@ -56,13 +211,91 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
|
|
||||||
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
||||||
|
|
||||||
if (error) {
|
if (errorGithubConnected instanceof Error) {
|
||||||
throw error;
|
return (
|
||||||
|
<div className="px-1 md:w-[653px]">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="mx-auto text-center">
|
||||||
|
<div className="mx-auto h-8 w-8">
|
||||||
|
<GitHubIcon className="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Text className="mt-2.5 text-center text-lg font-medium">
|
||||||
|
Error fetching GitHub data
|
||||||
|
</Text>
|
||||||
|
<ErrorMessage>{errorGithubConnected.message}</ErrorMessage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading || loadingProject || loadingOrg || loadingGithubConnected) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator delay={500} label="Loading GitHub repositories..." />
|
<div className="px-1 md:w-[653px]">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="mx-auto text-center">
|
||||||
|
<div className="mx-auto h-8 w-8">
|
||||||
|
<GitHubIcon className="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text className="mt-2.5 text-center text-lg font-medium">
|
||||||
|
Loading repositories...
|
||||||
|
</Text>
|
||||||
|
<Text className="text-center text-xs font-normal" color="secondary">
|
||||||
|
Fetching your GitHub repositories
|
||||||
|
</Text>
|
||||||
|
<div className="mb-2 mt-6 flex w-full">
|
||||||
|
<Input placeholder="Search..." fullWidth disabled value="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-import items-center justify-center border-y">
|
||||||
|
<ActivityIndicator delay={0} label="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ConnectGitHubModalState === 'GITHUB_CONNECTION_REQUIRED') {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-5 px-1 py-1 md:w-[653px]">
|
||||||
|
<p className="text-center text-foreground">
|
||||||
|
You need to connect your GitHub account to continue.
|
||||||
|
</p>
|
||||||
|
<NavLink
|
||||||
|
href={getGitHubConnectUrl()}
|
||||||
|
passHref
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
legacyBehavior
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="w-full max-w-72"
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<GitHubIcon />}
|
||||||
|
>
|
||||||
|
Connect to GitHub
|
||||||
|
</Button>
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ConnectGitHubModalState === 'EXPIRED_GITHUB_SESSION') {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-5 px-1 py-1 md:w-[653px]">
|
||||||
|
<p className="text-center text-foreground">
|
||||||
|
Please sign in with GitHub to continue.
|
||||||
|
</p>
|
||||||
|
<GithubAuthButton
|
||||||
|
redirectTo={`${hostname}?signinProvider=github&state=signin-refresh:${org.slug}:${project!.subdomain}`}
|
||||||
|
buttonText="Sign in with GitHub"
|
||||||
|
className="w-full max-w-72 gap-2 !bg-primary !text-white disabled:!text-white disabled:!text-opacity-60 dark:!bg-white dark:!text-black dark:disabled:!text-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,25 +311,27 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { githubAppInstallations } = data || {};
|
const { githubAppInstallations } = githubData || {};
|
||||||
|
|
||||||
const filteredGitHubAppInstallations = data?.githubAppInstallations.filter(
|
const filteredGitHubAppInstallations =
|
||||||
(githubApp) => !!githubApp.accountLogin,
|
githubData?.githubAppInstallations.filter(
|
||||||
);
|
(githubApp) => !!githubApp.accountLogin,
|
||||||
|
);
|
||||||
|
|
||||||
const filteredGitHubRepositories = data?.githubRepositories.filter(
|
const filteredGitHubRepositories = githubData?.githubRepositories.filter(
|
||||||
(repo) => !!repo.githubAppInstallation,
|
(repo) => !!repo.githubAppInstallation,
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredGitHubAppInstallationsNullValues =
|
const filteredGitHubAppInstallationsNullValues =
|
||||||
data?.githubAppInstallations.filter((githubApp) => !!githubApp.accountLogin)
|
githubData?.githubAppInstallations.filter(
|
||||||
.length === 0;
|
(githubApp) => !!githubApp.accountLogin,
|
||||||
|
).length === 0;
|
||||||
|
|
||||||
const faultyGitHubInstallation =
|
const faultyGitHubInstallation =
|
||||||
githubAppInstallations?.length === 0 ||
|
githubAppInstallations?.length === 0 ||
|
||||||
filteredGitHubAppInstallationsNullValues;
|
filteredGitHubAppInstallationsNullValues;
|
||||||
|
|
||||||
const noRepositoriesAdded = data?.githubRepositories.length === 0;
|
const noRepositoriesAdded = githubData?.githubRepositories.length === 0;
|
||||||
|
|
||||||
if (faultyGitHubInstallation) {
|
if (faultyGitHubInstallation) {
|
||||||
return (
|
return (
|
||||||
@@ -115,11 +350,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}
|
href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
|
||||||
// Both `target` and `rel` are available when `href` is set. This is
|
|
||||||
// a limitation of MUI.
|
|
||||||
// @ts-ignore
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
endIcon={<ArrowSquareOutIcon className="h-4 w-4" />}
|
endIcon={<ArrowSquareOutIcon className="h-4 w-4" />}
|
||||||
>
|
>
|
||||||
@@ -179,8 +410,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}
|
href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
className="grid grid-flow-col items-center justify-start gap-1"
|
className="grid grid-flow-col items-center justify-start gap-1"
|
||||||
@@ -199,8 +429,8 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
className="text-center text-xs font-normal"
|
className="text-center text-xs font-normal"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
>
|
>
|
||||||
Showing repositories from {data?.githubAppInstallations.length}{' '}
|
Showing repositories from{' '}
|
||||||
GitHub account(s)
|
{githubData?.githubAppInstallations.length} GitHub account(s)
|
||||||
</Text>
|
</Text>
|
||||||
<div className="mb-2 mt-6 flex w-full">
|
<div className="mb-2 mt-6 flex w-full">
|
||||||
<Input
|
<Input
|
||||||
@@ -226,7 +456,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => setSelectedRepoId(repo.id)}
|
onClick={() => setSelectedRepoId(repo.node_id)}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
@@ -268,8 +498,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
Do you miss a repository, or do you need to connect another GitHub
|
Do you miss a repository, or do you need to connect another GitHub
|
||||||
account?{' '}
|
account?{' '}
|
||||||
<Link
|
<Link
|
||||||
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}
|
href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
className="text-xs font-medium"
|
className="text-xs font-medium"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
|||||||
export interface EditRepositorySettingsProps {
|
export interface EditRepositorySettingsProps {
|
||||||
close?: () => void;
|
close?: () => void;
|
||||||
openConnectGithubModal?: () => void;
|
openConnectGithubModal?: () => void;
|
||||||
selectedRepoId?: string;
|
selectedRepoId: string;
|
||||||
connectGithubModalState?: ConnectGitHubModalState;
|
connectGithubModalState?: ConnectGitHubModalState;
|
||||||
handleSelectAnotherRepository?: () => void;
|
handleSelectAnotherRepository?: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { Text } from '@/components/ui/v2/Text';
|
|||||||
import { EditRepositoryAndBranchSettings } from '@/features/orgs/projects/git/common/components/EditRepositoryAndBranchSettings';
|
import { EditRepositoryAndBranchSettings } from '@/features/orgs/projects/git/common/components/EditRepositoryAndBranchSettings';
|
||||||
import type { EditRepositorySettingsFormData } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
|
import type { EditRepositorySettingsFormData } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
|
||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { useUpdateApplicationMutation } from '@/generated/graphql';
|
import { useConnectGithubRepoMutation } from '@/generated/graphql';
|
||||||
import { analytics } from '@/lib/segment';
|
import { analytics } from '@/lib/segment';
|
||||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||||
import { triggerToast } from '@/utils/toast';
|
import { triggerToast } from '@/utils/toast';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
export interface EditRepositorySettingsModalProps {
|
export interface EditRepositorySettingsModalProps {
|
||||||
selectedRepoId?: string;
|
selectedRepoId: string;
|
||||||
close?: () => void;
|
close?: () => void;
|
||||||
handleSelectAnotherRepository?: () => void;
|
handleSelectAnotherRepository?: () => void;
|
||||||
}
|
}
|
||||||
@@ -33,45 +33,29 @@ export default function EditRepositorySettingsModal({
|
|||||||
|
|
||||||
const { project, refetch: refetchProject } = useProject();
|
const { project, refetch: refetchProject } = useProject();
|
||||||
|
|
||||||
const [updateApp, { loading }] = useUpdateApplicationMutation();
|
const [connectGithubRepo, { loading }] = useConnectGithubRepoMutation();
|
||||||
|
|
||||||
const handleEditGitHubIntegration = async (
|
const handleEditGitHubIntegration = async (
|
||||||
data: EditRepositorySettingsFormData,
|
data: EditRepositorySettingsFormData,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (!project?.githubRepository || selectedRepoId) {
|
await connectGithubRepo({
|
||||||
await updateApp({
|
variables: {
|
||||||
variables: {
|
appID: project?.id,
|
||||||
appId: project?.id,
|
githubNodeID: selectedRepoId,
|
||||||
app: {
|
productionBranch: data.productionBranch,
|
||||||
githubRepositoryId: selectedRepoId,
|
baseFolder: data.repoBaseFolder,
|
||||||
repositoryProductionBranch: data.productionBranch,
|
},
|
||||||
nhostBaseFolder: data.repoBaseFolder,
|
});
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedRepoId) {
|
analytics.track('Project Connected to GitHub', {
|
||||||
analytics.track('Project Connected to GitHub', {
|
projectId: project?.id,
|
||||||
projectId: project?.id,
|
projectName: project?.name,
|
||||||
projectName: project?.name,
|
projectSubdomain: project?.subdomain,
|
||||||
projectSubdomain: project?.subdomain,
|
repositoryId: selectedRepoId,
|
||||||
repositoryId: selectedRepoId,
|
productionBranch: data.productionBranch,
|
||||||
productionBranch: data.productionBranch,
|
baseFolder: data.repoBaseFolder,
|
||||||
baseFolder: data.repoBaseFolder,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
appId: project.id,
|
|
||||||
app: {
|
|
||||||
repositoryProductionBranch: data.productionBranch,
|
|
||||||
nhostBaseFolder: data.repoBaseFolder,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await refetchProject();
|
await refetchProject();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
mutation ConnectGithubRepo(
|
||||||
|
$appID: uuid!
|
||||||
|
$githubNodeID: String!
|
||||||
|
$productionBranch: String!
|
||||||
|
$baseFolder: String!
|
||||||
|
) {
|
||||||
|
connectGithubRepo(
|
||||||
|
appID: $appID
|
||||||
|
githubNodeID: $githubNodeID
|
||||||
|
productionBranch: $productionBranch
|
||||||
|
baseFolder: $baseFolder
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,12 +2,12 @@ import { useDialog } from '@/components/common/DialogProvider';
|
|||||||
import { ConnectGitHubModal } from '@/features/orgs/projects/git/common/components/ConnectGitHubModal';
|
import { ConnectGitHubModal } from '@/features/orgs/projects/git/common/components/ConnectGitHubModal';
|
||||||
|
|
||||||
export default function useGitHubModal() {
|
export default function useGitHubModal() {
|
||||||
const { openAlertDialog } = useDialog();
|
const { openAlertDialog, closeAlertDialog } = useDialog();
|
||||||
|
|
||||||
function openGitHubModal() {
|
function openGitHubModal() {
|
||||||
openAlertDialog({
|
openAlertDialog({
|
||||||
title: 'Connect GitHub Repository',
|
title: 'Connect GitHub Repository',
|
||||||
payload: <ConnectGitHubModal />,
|
payload: <ConnectGitHubModal close={closeAlertDialog} />,
|
||||||
props: {
|
props: {
|
||||||
hidePrimaryAction: true,
|
hidePrimaryAction: true,
|
||||||
hideSecondaryAction: true,
|
hideSecondaryAction: true,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { isNotEmptyValue } from '@/lib/utils';
|
||||||
|
import type { ProviderSession } from '@nhost/nhost-js/auth';
|
||||||
|
|
||||||
|
const githubProviderTokenKey = 'nhost_provider_tokens_github';
|
||||||
|
|
||||||
|
export type GitHubProviderToken = ProviderSession & {
|
||||||
|
authUserProviderId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function saveGitHubToken(token: GitHubProviderToken) {
|
||||||
|
localStorage.setItem(githubProviderTokenKey, JSON.stringify(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGitHubToken() {
|
||||||
|
const token = localStorage.getItem(githubProviderTokenKey);
|
||||||
|
return isNotEmptyValue(token)
|
||||||
|
? (JSON.parse(token) as GitHubProviderToken)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearGitHubToken() {
|
||||||
|
localStorage.removeItem(githubProviderTokenKey);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './githubTokens';
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Button } from '@/components/ui/v3/button';
|
||||||
|
import { useDataGridCustomizerOpenStateContext } from '@/features/orgs/projects/common/components/DataGridCustomizerControls/DataGridCustomizerOpenStateProvider';
|
||||||
|
import { DataBrowserEmptyState } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserEmptyState';
|
||||||
|
|
||||||
|
function Description() {
|
||||||
|
const { setOpen } = useDataGridCustomizerOpenStateContext();
|
||||||
|
|
||||||
|
function handleOnClick() {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant="link" onClick={handleOnClick} className="pr-2">
|
||||||
|
Open Customize Table View
|
||||||
|
</Button>
|
||||||
|
to choose which columns to display.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AllColumnsHiddenMessage() {
|
||||||
|
return (
|
||||||
|
<DataBrowserEmptyState
|
||||||
|
title="All columns are hidden"
|
||||||
|
description={<Description />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AllColumnsHiddenMessage;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen } from '@/tests/testUtils';
|
import { render, screen } from '@/tests/testUtils';
|
||||||
import type { Column } from 'react-table';
|
import type { Column } from 'react-table';
|
||||||
import { expect, test } from 'vitest';
|
import { expect, it } from 'vitest';
|
||||||
import DataGrid from './DataGrid';
|
import DataGrid from './DataGrid';
|
||||||
|
|
||||||
interface MockDataDetails {
|
interface MockDataDetails {
|
||||||
@@ -18,50 +18,54 @@ const mockData: MockDataDetails[] = [
|
|||||||
{ id: 2, name: 'bar' },
|
{ id: 2, name: 'bar' },
|
||||||
];
|
];
|
||||||
|
|
||||||
test('should render an empty state if columns are not available', () => {
|
describe('DataGrid', () => {
|
||||||
render(<DataGrid columns={[]} data={[]} />);
|
it('should render an empty state if columns are not available', () => {
|
||||||
|
render(<DataGrid columns={[]} data={[]} />);
|
||||||
|
|
||||||
expect(screen.getByText(/columns not found/i)).toBeInTheDocument();
|
expect(screen.getByText(/columns not found/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render columns and empty state message if data is unavailable', () => {
|
it('should render columns and empty state message if data is unavailable', () => {
|
||||||
render(<DataGrid columns={mockColumns} data={[]} />);
|
render(<DataGrid columns={mockColumns} data={[]} />);
|
||||||
|
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByRole('columnheader', { name: /id/i })).toBeInTheDocument();
|
expect(
|
||||||
expect(
|
screen.getByRole('columnheader', { name: /id/i }),
|
||||||
screen.getByRole('columnheader', { name: /name/i }),
|
).toBeInTheDocument();
|
||||||
).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByRole('columnheader', { name: /name/i }),
|
||||||
expect(screen.getByText(/no data is available/i)).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
|
||||||
|
expect(screen.getByText(/no data is available/i)).toBeInTheDocument();
|
||||||
test('should render custom empty state message if data is unavailable', () => {
|
});
|
||||||
const customEmptyStateMessage = 'custom empty state message';
|
|
||||||
|
it('should render custom empty state message if data is unavailable', () => {
|
||||||
render(
|
const customEmptyStateMessage = 'custom empty state message';
|
||||||
<DataGrid
|
|
||||||
columns={mockColumns}
|
render(
|
||||||
data={[]}
|
<DataGrid
|
||||||
emptyStateMessage={customEmptyStateMessage}
|
columns={mockColumns}
|
||||||
/>,
|
data={[]}
|
||||||
);
|
emptyStateMessage={customEmptyStateMessage}
|
||||||
|
/>,
|
||||||
expect(screen.getByText(customEmptyStateMessage)).toBeInTheDocument();
|
);
|
||||||
});
|
|
||||||
|
expect(screen.getByText(customEmptyStateMessage)).toBeInTheDocument();
|
||||||
test('should display a loading indicator', async () => {
|
});
|
||||||
render(<DataGrid columns={mockColumns} data={[]} loading />);
|
|
||||||
|
it('should display a loading indicator', async () => {
|
||||||
// Activity indicator is not immediately displayed, so we need to wait
|
render(<DataGrid columns={mockColumns} data={[]} loading />);
|
||||||
expect(await screen.findByRole('progressbar')).toBeInTheDocument();
|
|
||||||
});
|
// Activity indicator is not immediately displayed, so we need to wait
|
||||||
|
expect(await screen.findByRole('progressbar')).toBeInTheDocument();
|
||||||
test('should render data if provided', () => {
|
});
|
||||||
render(<DataGrid columns={mockColumns} data={mockData} />);
|
|
||||||
|
it('should render data if provided', () => {
|
||||||
expect(screen.getAllByRole('row')).toHaveLength(2);
|
render(<DataGrid columns={mockColumns} data={mockData} />);
|
||||||
expect(screen.getByRole('cell', { name: /1/i })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('cell', { name: /foo/i })).toBeInTheDocument();
|
expect(screen.getAllByRole('row')).toHaveLength(2);
|
||||||
|
expect(screen.getByRole('cell', { name: /1/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('cell', { name: /foo/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { Spinner } from '@/components/ui/v3/spinner';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import DataGridCustomizerOpenStateProvider from '@/features/orgs/projects/common/components/DataGridCustomizerControls/DataGridCustomizerOpenStateProvider';
|
||||||
import { DataBrowserEmptyState } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserEmptyState';
|
import { DataBrowserEmptyState } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserEmptyState';
|
||||||
import type { UseDataGridOptions } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid/useDataGrid';
|
import type { UseDataGridOptions } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid/useDataGrid';
|
||||||
import { DataGridBody } from '@/features/orgs/projects/storage/dataGrid/components/DataGridBody';
|
import { DataGridBody } from '@/features/orgs/projects/storage/dataGrid/components/DataGridBody';
|
||||||
@@ -7,11 +7,13 @@ import { DataGridConfigProvider } from '@/features/orgs/projects/storage/dataGri
|
|||||||
import { DataGridFrame } from '@/features/orgs/projects/storage/dataGrid/components/DataGridFrame';
|
import { DataGridFrame } from '@/features/orgs/projects/storage/dataGrid/components/DataGridFrame';
|
||||||
import type { DataGridHeaderProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
|
import type { DataGridHeaderProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
|
||||||
import { DataGridHeader } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
|
import { DataGridHeader } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
|
||||||
|
import { DataTableDesignProvider } from '@/features/orgs/projects/storage/dataGrid/providers/DataTableDesignProvider';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import type { ForwardedRef } from 'react';
|
import type { ForwardedRef } from 'react';
|
||||||
import { forwardRef, useEffect, useRef } from 'react';
|
import { forwardRef, useEffect, useRef } from 'react';
|
||||||
import { mergeRefs } from 'react-merge-refs';
|
import { mergeRefs } from 'react-merge-refs';
|
||||||
import type { Column, Row, SortingRule, TableOptions } from 'react-table';
|
import type { Column, Row, SortingRule, TableOptions } from 'react-table';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import AllColumnsHiddenMessage from './AllColumnsHiddenMessage';
|
||||||
import useDataGrid from './useDataGrid';
|
import useDataGrid from './useDataGrid';
|
||||||
|
|
||||||
export interface DataGridProps<TColumnData extends object>
|
export interface DataGridProps<TColumnData extends object>
|
||||||
@@ -65,6 +67,10 @@ export interface DataGridProps<TColumnData extends object>
|
|||||||
* Props to be passed to the `DataGridHeader` component.
|
* Props to be passed to the `DataGridHeader` component.
|
||||||
*/
|
*/
|
||||||
headerProps?: DataGridHeaderProps;
|
headerProps?: DataGridHeaderProps;
|
||||||
|
/**
|
||||||
|
* Determines whether the Grid is used for displaying files.
|
||||||
|
*/
|
||||||
|
isFileDataGrid?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataGrid<TColumnData extends object>(
|
function DataGrid<TColumnData extends object>(
|
||||||
@@ -82,6 +88,7 @@ function DataGrid<TColumnData extends object>(
|
|||||||
onSort,
|
onSort,
|
||||||
loading,
|
loading,
|
||||||
className,
|
className,
|
||||||
|
isFileDataGrid,
|
||||||
}: DataGridProps<TColumnData>,
|
}: DataGridProps<TColumnData>,
|
||||||
ref: ForwardedRef<HTMLDivElement>,
|
ref: ForwardedRef<HTMLDivElement>,
|
||||||
) {
|
) {
|
||||||
@@ -112,6 +119,8 @@ function DataGrid<TColumnData extends object>(
|
|||||||
}
|
}
|
||||||
}, [allowSort, dataGridProps.state.sortBy, onSort, toggleAllRowsSelected]);
|
}, [allowSort, dataGridProps.state.sortBy, onSort, toggleAllRowsSelected]);
|
||||||
|
|
||||||
|
const allColumnsHidden =
|
||||||
|
dataGridProps.allColumns.filter(({ isVisible }) => isVisible).length === 1;
|
||||||
return (
|
return (
|
||||||
<DataGridConfigProvider
|
<DataGridConfigProvider
|
||||||
toggleAllRowsSelected={toggleAllRowsSelected}
|
toggleAllRowsSelected={toggleAllRowsSelected}
|
||||||
@@ -119,38 +128,43 @@ function DataGrid<TColumnData extends object>(
|
|||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
{...dataGridProps}
|
{...dataGridProps}
|
||||||
>
|
>
|
||||||
<>
|
<DataTableDesignProvider>
|
||||||
{controls}
|
<DataGridCustomizerOpenStateProvider>
|
||||||
{columns.length === 0 && !loading && (
|
<>
|
||||||
<DataBrowserEmptyState
|
{controls}
|
||||||
title="Columns not found"
|
{columns.length === 0 && !loading && (
|
||||||
description="Please create a column before adding data to the table."
|
<DataBrowserEmptyState
|
||||||
/>
|
title="Columns not found"
|
||||||
)}
|
description="Please create a column before adding data to the table."
|
||||||
|
|
||||||
{columns.length > 0 && (
|
|
||||||
<Box
|
|
||||||
ref={mergeRefs([ref, tableRef])}
|
|
||||||
sx={{ backgroundColor: 'background.default' }}
|
|
||||||
className={twMerge(
|
|
||||||
'overflow-x-auto',
|
|
||||||
!loading && 'h-full',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DataGridFrame>
|
|
||||||
<DataGridHeader {...headerProps} />
|
|
||||||
|
|
||||||
<DataGridBody
|
|
||||||
emptyStateMessage={emptyStateMessage}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
/>
|
||||||
</DataGridFrame>
|
)}
|
||||||
</Box>
|
{columns.length > 0 && allColumnsHidden && (
|
||||||
)}
|
<AllColumnsHiddenMessage />
|
||||||
|
)}
|
||||||
|
{columns.length > 0 && !allColumnsHidden && (
|
||||||
|
<div
|
||||||
|
ref={mergeRefs([ref, tableRef])}
|
||||||
|
className={cn(
|
||||||
|
'box overflow-x-auto bg-background',
|
||||||
|
{ 'h-[calc(100%-1px)]': !loading }, // need to set height like this to remove vertical scrollbar
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DataGridFrame>
|
||||||
|
<DataGridHeader {...headerProps} />
|
||||||
|
<DataGridBody
|
||||||
|
isFileDataGrid={isFileDataGrid}
|
||||||
|
emptyStateMessage={emptyStateMessage}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</DataGridFrame>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading && <ActivityIndicator delay={1000} className="my-4" />}
|
{loading && <Spinner className="my-4" />}
|
||||||
</>
|
</>
|
||||||
|
</DataGridCustomizerOpenStateProvider>
|
||||||
|
</DataTableDesignProvider>
|
||||||
</DataGridConfigProvider>
|
</DataGridConfigProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
import { Checkbox } from '@/components/ui/v3/checkbox';
|
||||||
|
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
|
||||||
|
import PersistenColumnConfigurationStorage from '@/features/orgs/projects/storage/dataGrid/utils/PersistenDataTableConfigurationStorage';
|
||||||
import type { MutableRefObject } from 'react';
|
import type { MutableRefObject } from 'react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { PluginHook, TableInstance, TableOptions } from 'react-table';
|
import type { PluginHook, TableInstance, TableOptions } from 'react-table';
|
||||||
import {
|
import {
|
||||||
useBlockLayout,
|
useBlockLayout,
|
||||||
|
useColumnOrder,
|
||||||
useResizeColumns,
|
useResizeColumns,
|
||||||
useRowSelect,
|
useRowSelect,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
@@ -57,17 +60,26 @@ export default function useDataGrid<T extends object>(
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tablePath = useTablePath();
|
||||||
|
|
||||||
const pluginHooks = [
|
const pluginHooks = [
|
||||||
useBlockLayout,
|
useBlockLayout,
|
||||||
useResizeColumns,
|
useResizeColumns,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
useRowSelect,
|
useRowSelect,
|
||||||
|
useColumnOrder,
|
||||||
];
|
];
|
||||||
|
|
||||||
const tableData = useTable<T>(
|
const tableData = useTable<T>(
|
||||||
{
|
{
|
||||||
defaultColumn,
|
defaultColumn,
|
||||||
...options,
|
...options,
|
||||||
|
initialState: {
|
||||||
|
hiddenColumns:
|
||||||
|
PersistenColumnConfigurationStorage.getHiddenColumns(tablePath),
|
||||||
|
columnOrder:
|
||||||
|
PersistenColumnConfigurationStorage.getColumnOrder(tablePath),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...pluginHooks,
|
...pluginHooks,
|
||||||
...plugins,
|
...plugins,
|
||||||
@@ -76,25 +88,43 @@ export default function useDataGrid<T extends object>(
|
|||||||
? hooks.visibleColumns.push((columns) => [
|
? hooks.visibleColumns.push((columns) => [
|
||||||
{
|
{
|
||||||
id: 'selection-column',
|
id: 'selection-column',
|
||||||
Header: ({ rows, getToggleAllRowsSelectedProps }: any) => (
|
Header: ({ rows, getToggleAllRowsSelectedProps }: any) => {
|
||||||
<Checkbox
|
const { indeterminate, style, onChange, ...props } =
|
||||||
disabled={rows.length === 0}
|
getToggleAllRowsSelectedProps();
|
||||||
{...getToggleAllRowsSelectedProps({ style: null })}
|
|
||||||
style={{
|
function handleCheckedChange(newCheckedState: boolean) {
|
||||||
...getToggleAllRowsSelectedProps().style,
|
onChange({ target: { checked: newCheckedState } });
|
||||||
cursor: rows.length === 0 ? 'default' : 'pointer',
|
}
|
||||||
}}
|
return (
|
||||||
/>
|
<Checkbox
|
||||||
),
|
className="border-[#21324b] data-[state=checked]:!border-transparent dark:border-[#dfecf5]"
|
||||||
|
disabled={rows.length === 0}
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
cursor: rows.length === 0 ? 'default' : 'pointer',
|
||||||
|
}}
|
||||||
|
onCheckedChange={handleCheckedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
Cell: ({ row }: any) => {
|
Cell: ({ row }: any) => {
|
||||||
const originalValue = row.original as any;
|
const originalValue = row.original as any;
|
||||||
|
|
||||||
|
const { indeterminate, onChange, ...props } =
|
||||||
|
row.getToggleRowSelectedProps();
|
||||||
|
|
||||||
|
function handleCheckedChange(newCheckedState: boolean) {
|
||||||
|
onChange({ target: { checked: newCheckedState } });
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
{...row.getToggleRowSelectedProps()}
|
className="border-[#21324b] data-[state=checked]:!border-transparent dark:border-[#dfecf5]"
|
||||||
|
{...props}
|
||||||
// disable selection if row is just a upload preview
|
// disable selection if row is just a upload preview
|
||||||
checked={originalValue.uploading ? false : row.isSelected}
|
checked={originalValue.uploading ? false : row.isSelected}
|
||||||
disabled={originalValue.uploading}
|
disabled={originalValue.uploading}
|
||||||
|
onCheckedChange={handleCheckedChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { Box } from '@/components/ui/v2/Box';
|
|
||||||
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||||
import type { DataGridProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid/DataGrid';
|
import type { DataGridProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid/DataGrid';
|
||||||
import { DataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
import { DataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||||
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||||
import { isNotEmptyValue } from '@/lib/utils';
|
import { useDataTableDesignContext } from '@/features/orgs/projects/storage/dataGrid/providers/DataTableDesignProvider';
|
||||||
|
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||||
import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react';
|
import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import type { Row } from 'react-table';
|
import type { Row } from 'react-table';
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
export interface DataGridBodyProps<T extends object>
|
export interface DataGridBodyProps<T extends object>
|
||||||
extends Omit<
|
extends Omit<
|
||||||
@@ -15,10 +14,7 @@ export interface DataGridBodyProps<T extends object>
|
|||||||
'children'
|
'children'
|
||||||
>,
|
>,
|
||||||
Pick<DataGridProps<T>, 'emptyStateMessage' | 'loading'> {
|
Pick<DataGridProps<T>, 'emptyStateMessage' | 'loading'> {
|
||||||
/**
|
isFileDataGrid?: boolean;
|
||||||
* Determines whether column insertion is allowed.
|
|
||||||
*/
|
|
||||||
allowInsertColumn?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Get rid of Data Browser related code from here. This component should
|
// TODO: Get rid of Data Browser related code from here. This component should
|
||||||
@@ -26,11 +22,12 @@ export interface DataGridBodyProps<T extends object>
|
|||||||
export default function DataGridBody<T extends object>({
|
export default function DataGridBody<T extends object>({
|
||||||
emptyStateMessage = 'No data is available',
|
emptyStateMessage = 'No data is available',
|
||||||
loading,
|
loading,
|
||||||
allowInsertColumn,
|
isFileDataGrid,
|
||||||
...props
|
...props
|
||||||
}: DataGridBodyProps<T>) {
|
}: DataGridBodyProps<T>) {
|
||||||
const { getTableBodyProps, totalColumnsWidth, rows, prepareRow } =
|
const { getTableBodyProps, totalColumnsWidth, rows, prepareRow } =
|
||||||
useDataGridConfig<T>();
|
useDataGridConfig<T>();
|
||||||
|
const context = useDataTableDesignContext();
|
||||||
|
|
||||||
const bodyRef = useRef<HTMLDivElement | null>(null);
|
const bodyRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@@ -137,35 +134,18 @@ export default function DataGridBody<T extends object>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBackgroundCellColor = (
|
|
||||||
row: Row<T>,
|
|
||||||
column: DataBrowserGridColumn<T>,
|
|
||||||
) => {
|
|
||||||
// Grey out files not uploaded
|
|
||||||
if (!row.values.isUploaded) {
|
|
||||||
return 'grey.200';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.isDisabled) {
|
|
||||||
return 'grey.100';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'background.paper';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
|
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
|
||||||
{rows.length === 0 && !loading && (
|
{rows.length === 0 && !loading && (
|
||||||
<div className="flex flex-nowrap">
|
<div className="flex flex-nowrap">
|
||||||
<Box
|
<div
|
||||||
className="inline-flex h-12 items-center border-b-1 border-r-1 px-2 py-1.5 text-xs"
|
className="box inline-flex h-12 items-center border-b-1 border-r-1 px-2 py-1.5 text-xs dark:!text-[#a2b3be]"
|
||||||
sx={{ color: 'text.secondary' }}
|
|
||||||
style={{
|
style={{
|
||||||
width: totalColumnsWidth,
|
width: totalColumnsWidth,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{emptyStateMessage}
|
{emptyStateMessage}
|
||||||
</Box>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -174,6 +154,7 @@ export default function DataGridBody<T extends object>({
|
|||||||
|
|
||||||
const rowProps = row.getRowProps({
|
const rowProps = row.getRowProps({
|
||||||
style: {
|
style: {
|
||||||
|
height: context.rowDensity === 'comfortable' ? '3rem' : '2rem',
|
||||||
width: totalColumnsWidth,
|
width: totalColumnsWidth,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -182,7 +163,12 @@ export default function DataGridBody<T extends object>({
|
|||||||
<div
|
<div
|
||||||
{...rowProps}
|
{...rowProps}
|
||||||
id={row.id}
|
id={row.id}
|
||||||
className="flex scroll-mt-10"
|
className={cn(
|
||||||
|
'flex scroll-mt-10 border-b-1 border-b-transparent last:border-b-data-table-border-color',
|
||||||
|
isFileDataGrid && !row.values.isUploaded
|
||||||
|
? 'bg-disabled'
|
||||||
|
: 'odd:bg-data-cell-bg-odd even:bg-data-cell-bg hover:bg-data-cell-bg-hover',
|
||||||
|
)}
|
||||||
role="row"
|
role="row"
|
||||||
onKeyDown={(event) => handleKeyDown(event, row)}
|
onKeyDown={(event) => handleKeyDown(event, row)}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
@@ -198,23 +184,16 @@ export default function DataGridBody<T extends object>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DataGridCell
|
<DataGridCell
|
||||||
{...cell.getCellProps({
|
{...cell.getCellProps()}
|
||||||
style: {
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
cell={cell}
|
cell={cell}
|
||||||
sx={{
|
className={cn(
|
||||||
backgroundColor: getBackgroundCellColor(row, column),
|
'group !inline-flex items-center bg-inherit font-display text-xs',
|
||||||
color: isCellDisabled ? 'text.secondary' : 'text.primary',
|
'border-b-0 border-r-1',
|
||||||
}}
|
|
||||||
className={twMerge(
|
|
||||||
'h-12 font-display text-xs motion-safe:transition-colors',
|
|
||||||
'border-b-1 border-r-1',
|
|
||||||
'scroll-ml-8 scroll-mt-[57px]',
|
'scroll-ml-8 scroll-mt-[57px]',
|
||||||
|
'border-r-transparent last:border-r-data-table-border-color',
|
||||||
column.id === 'selection-column' &&
|
column.id === 'selection-column' &&
|
||||||
'sticky left-0 z-20 justify-center px-0',
|
'sticky left-0 z-20 justify-center px-0',
|
||||||
|
isCellDisabled ? 'text-secondary' : 'text-primary-text',
|
||||||
)}
|
)}
|
||||||
isEditable={!column.isDisabled && column.isEditable}
|
isEditable={!column.isDisabled && column.isEditable}
|
||||||
id={cellIndex.toString()}
|
id={cellIndex.toString()}
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
|
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
|
||||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/v3/dropdown-menu';
|
||||||
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||||
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||||
import type { MouseEvent, KeyboardEvent as ReactKeyboardEvent } from 'react';
|
import {
|
||||||
import { twMerge } from 'tailwind-merge';
|
useState,
|
||||||
|
type MouseEvent,
|
||||||
|
type KeyboardEvent as ReactKeyboardEvent,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
export type DataGridBooleanCellProps<TData extends object> =
|
export interface DataGridBooleanCellProps<TData extends object>
|
||||||
CommonDataGridCellProps<TData, boolean | null | undefined>;
|
extends CommonDataGridCellProps<TData, boolean | null | undefined> {
|
||||||
|
rowId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function DataGridBooleanCell<TData extends object>({
|
export default function DataGridBooleanCell<TData extends object>({
|
||||||
onSave,
|
onSave,
|
||||||
optimisticValue,
|
optimisticValue,
|
||||||
temporaryValue,
|
|
||||||
onTemporaryValueChange,
|
onTemporaryValueChange,
|
||||||
|
rowId,
|
||||||
cell: {
|
cell: {
|
||||||
column: { isNullable },
|
column: { isNullable, getHeaderProps },
|
||||||
},
|
},
|
||||||
}: DataGridBooleanCellProps<TData>) {
|
}: DataGridBooleanCellProps<TData>) {
|
||||||
const {
|
const { inputRef, focusCell, cancelEditCell, focusNextCell, editCell } =
|
||||||
inputRef,
|
useDataGridCell<HTMLButtonElement>();
|
||||||
isEditing,
|
const [open, setOpen] = useState(false);
|
||||||
focusCell,
|
|
||||||
editCell,
|
|
||||||
cancelEditCell,
|
|
||||||
isSelected,
|
|
||||||
} = useDataGridCell<HTMLButtonElement>();
|
|
||||||
|
|
||||||
async function handleMenuClick(
|
async function handleMenuClick(
|
||||||
event: MouseEvent<HTMLLIElement> | ReactKeyboardEvent<HTMLLIElement>,
|
event: MouseEvent<HTMLDivElement> | ReactKeyboardEvent<HTMLDivElement>,
|
||||||
value: boolean | null,
|
value: boolean | null,
|
||||||
) {
|
) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -48,74 +53,83 @@ export default function DataGridBooleanCell<TData extends object>({
|
|||||||
// We need to restore the temporary value, because editing was cancelled
|
// We need to restore the temporary value, because editing was cancelled
|
||||||
if (event.key === 'Escape' && onTemporaryValueChange) {
|
if (event.key === 'Escape' && onTemporaryValueChange) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
onTemporaryValueChange(optimisticValue);
|
onTemporaryValueChange(optimisticValue);
|
||||||
cancelEditCell();
|
cancelEditCell();
|
||||||
|
focusCell();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Tab' && onSave) {
|
if (event.key === 'Tab' && onSave) {
|
||||||
await onSave(temporaryValue);
|
setOpen(false);
|
||||||
cancelEditCell();
|
focusNextCell();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTemporaryValueChange(value: boolean | null) {
|
function handleTemporaryValueChange(
|
||||||
if (onTemporaryValueChange) {
|
event: ReactKeyboardEvent<HTMLDivElement>,
|
||||||
|
value: boolean | null,
|
||||||
|
) {
|
||||||
|
if (onTemporaryValueChange && event.key === 'Enter') {
|
||||||
onTemporaryValueChange(value);
|
onTemporaryValueChange(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// needed to open with enter
|
||||||
|
function handleTriggerClick(event: MouseEvent) {
|
||||||
|
if (!open && !event.nativeEvent.isTrusted) {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return isSelected ? (
|
function handleOpenChange(newOpenState: boolean) {
|
||||||
<Dropdown.Root id="boolean-data-editor" className="h-full w-full">
|
if (newOpenState) {
|
||||||
<Dropdown.Trigger
|
editCell();
|
||||||
id="boolean-trigger"
|
} else {
|
||||||
className={twMerge(
|
cancelEditCell();
|
||||||
'h-full w-full border-none p-0 outline-none',
|
}
|
||||||
isEditing && 'p-1.5',
|
setOpen(newOpenState);
|
||||||
)}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={open} onOpenChange={handleOpenChange} modal={false}>
|
||||||
|
<DropdownMenuTrigger
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onClick={editCell}
|
className="h-full w-full focus-visible:border-transparent focus-visible:outline-none"
|
||||||
autoFocus={false}
|
onClick={handleTriggerClick}
|
||||||
sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}
|
|
||||||
>
|
>
|
||||||
<ReadOnlyToggle checked={optimisticValue} />
|
<ReadOnlyToggle checked={optimisticValue} />
|
||||||
</Dropdown.Trigger>
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
<Dropdown.Content
|
id={rowId}
|
||||||
menu
|
style={{ width: getHeaderProps().style?.width }}
|
||||||
disablePortal
|
|
||||||
onKeyDown={handleMenuKeyDown}
|
onKeyDown={handleMenuKeyDown}
|
||||||
PaperProps={{ className: 'w-[200px]' }}
|
className="rounded-none border-2 !border-t-0 border-[#0052cd] bg-data-cell-bg"
|
||||||
TransitionProps={{ onExited: focusCell }}
|
|
||||||
>
|
>
|
||||||
<Dropdown.Item
|
<DropdownMenuCheckboxItem
|
||||||
selected={optimisticValue === true}
|
className="hover:!bg-data-cell-bg-hover"
|
||||||
onKeyUp={() => handleTemporaryValueChange(true)}
|
checked={optimisticValue === true}
|
||||||
|
onKeyUp={(event) => handleTemporaryValueChange(event, true)}
|
||||||
onClick={(event) => handleMenuClick(event, true)}
|
onClick={(event) => handleMenuClick(event, true)}
|
||||||
>
|
>
|
||||||
<ReadOnlyToggle checked />
|
<ReadOnlyToggle checked />
|
||||||
</Dropdown.Item>
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
<Dropdown.Item
|
className="hover:!bg-data-cell-bg-hover"
|
||||||
selected={optimisticValue === false}
|
checked={optimisticValue === false}
|
||||||
onKeyUp={() => handleTemporaryValueChange(false)}
|
onKeyUp={(event) => handleTemporaryValueChange(event, false)}
|
||||||
onClick={(event) => handleMenuClick(event, false)}
|
onClick={(event) => handleMenuClick(event, false)}
|
||||||
>
|
>
|
||||||
<ReadOnlyToggle checked={false} />
|
<ReadOnlyToggle checked={false} />
|
||||||
</Dropdown.Item>
|
</DropdownMenuCheckboxItem>
|
||||||
|
|
||||||
{isNullable && (
|
{isNullable && (
|
||||||
<Dropdown.Item
|
<DropdownMenuCheckboxItem
|
||||||
selected={optimisticValue === null}
|
className="hover:!bg-data-cell-bg-hover"
|
||||||
onKeyUp={() => handleTemporaryValueChange(null)}
|
checked={optimisticValue === null}
|
||||||
|
onKeyUp={(event) => handleTemporaryValueChange(event, null)}
|
||||||
onClick={(event) => handleMenuClick(event, null)}
|
onClick={(event) => handleMenuClick(event, null)}
|
||||||
>
|
>
|
||||||
<ReadOnlyToggle checked={null} />
|
<ReadOnlyToggle checked={null} />
|
||||||
</Dropdown.Item>
|
</DropdownMenuCheckboxItem>
|
||||||
)}
|
)}
|
||||||
</Dropdown.Content>
|
</DropdownMenuContent>
|
||||||
</Dropdown.Root>
|
</DropdownMenu>
|
||||||
) : (
|
|
||||||
<ReadOnlyToggle checked={optimisticValue} />
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
import { useTooltip } from '@/components/ui/v2/Tooltip';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import { Button } from '@/components/ui/v3/button';
|
||||||
import { Tooltip, useTooltip } from '@/components/ui/v2/Tooltip';
|
|
||||||
import type {
|
import type {
|
||||||
ColumnType,
|
ColumnType,
|
||||||
DataBrowserGridCell,
|
DataBrowserGridCell,
|
||||||
DataBrowserGridCellProps,
|
DataBrowserGridCellProps,
|
||||||
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||||
|
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||||
|
import { copy } from '@/utils/copy';
|
||||||
import { triggerToast } from '@/utils/toast';
|
import { triggerToast } from '@/utils/toast';
|
||||||
|
import { Copy } from 'lucide-react';
|
||||||
import type {
|
import type {
|
||||||
|
CSSProperties,
|
||||||
FocusEvent,
|
FocusEvent,
|
||||||
JSXElementConstructor,
|
JSXElementConstructor,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
|
PropsWithChildren,
|
||||||
ReactElement,
|
ReactElement,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
ReactPortal,
|
ReactPortal,
|
||||||
@@ -24,10 +28,15 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
import DataGridCellProvider from './DataGridCellProvider';
|
import DataGridCellProvider from './DataGridCellProvider';
|
||||||
import useDataGridCell from './useDataGridCell';
|
import useDataGridCell from './useDataGridCell';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/v3/tooltip';
|
||||||
|
|
||||||
export interface CommonDataGridCellProps<TData extends object, TValue = any>
|
export interface CommonDataGridCellProps<TData extends object, TValue = any>
|
||||||
extends DataBrowserGridCellProps<TData, TValue> {
|
extends DataBrowserGridCellProps<TData, TValue> {
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +63,7 @@ export interface CommonDataGridCellProps<TData extends object, TValue = any>
|
|||||||
onTemporaryValueChange?: (value: TValue) => void;
|
onTemporaryValueChange?: (value: TValue) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataGridCellProps<TData extends object> extends BoxProps {
|
export interface DataGridCellProps<TData extends object> {
|
||||||
/**
|
/**
|
||||||
* Current cell's props.
|
* Current cell's props.
|
||||||
*/
|
*/
|
||||||
@@ -67,6 +76,9 @@ export interface DataGridCellProps<TData extends object> extends BoxProps {
|
|||||||
* Determines the column's type.
|
* Determines the column's type.
|
||||||
*/
|
*/
|
||||||
columnType?: ColumnType;
|
columnType?: ColumnType;
|
||||||
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataGridCellContent<TData extends object = {}>({
|
function DataGridCellContent<TData extends object = {}>({
|
||||||
@@ -79,7 +91,7 @@ function DataGridCellContent<TData extends object = {}>({
|
|||||||
row,
|
row,
|
||||||
},
|
},
|
||||||
...props
|
...props
|
||||||
}: DataGridCellProps<TData>) {
|
}: PropsWithChildren<DataGridCellProps<TData>>) {
|
||||||
const { openAlertDialog } = useDialog();
|
const { openAlertDialog } = useDialog();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -194,6 +206,8 @@ function DataGridCellContent<TData extends object = {}>({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
focusCell();
|
||||||
|
cancelEditCell();
|
||||||
// Syncing optimistic value with server-side value
|
// Syncing optimistic value with server-side value
|
||||||
setTemporaryValue(data.original[id.toString()]);
|
setTemporaryValue(data.original[id.toString()]);
|
||||||
setOptimisticValue(data.original[id.toString()]);
|
setOptimisticValue(data.original[id.toString()]);
|
||||||
@@ -203,17 +217,31 @@ function DataGridCellContent<TData extends object = {}>({
|
|||||||
// Resetting values
|
// Resetting values
|
||||||
setTemporaryValue(latestOptimisticValue);
|
setTemporaryValue(latestOptimisticValue);
|
||||||
setOptimisticValue(latestOptimisticValue);
|
setOptimisticValue(latestOptimisticValue);
|
||||||
|
activateInput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBlur(event: FocusEvent<HTMLDivElement>) {
|
async function handleBlur(event: FocusEvent<HTMLDivElement>) {
|
||||||
// We are deselecting cell only if focus target is not a descendant of it.
|
// We are deselecting cell only if focus target is not a descendant of it.
|
||||||
if (!isEditable || event.currentTarget.contains(event.relatedTarget)) {
|
const { id: currentRowId } = row.original as { id: string };
|
||||||
|
const isTargetDropdownMenu =
|
||||||
|
event.relatedTarget?.id === currentRowId ||
|
||||||
|
event.relatedTarget?.parentElement?.id === currentRowId;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isEditable ||
|
||||||
|
event.currentTarget.contains(event.relatedTarget) ||
|
||||||
|
(isEditing && type === 'boolean' && isTargetDropdownMenu)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleSave(temporaryValue);
|
if (type !== 'boolean') {
|
||||||
closeTooltip();
|
await handleSave(temporaryValue);
|
||||||
|
}
|
||||||
|
if (tooltipOpen) {
|
||||||
|
closeTooltip();
|
||||||
|
}
|
||||||
deselectCell();
|
deselectCell();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,8 +255,7 @@ function DataGridCellContent<TData extends object = {}>({
|
|||||||
if (!isNullable) {
|
if (!isNullable) {
|
||||||
openTooltip(
|
openTooltip(
|
||||||
<span>
|
<span>
|
||||||
<strong>{id}</strong>
|
<strong>{id}</strong> is non-nullable.
|
||||||
is non-nullable.
|
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -308,14 +335,14 @@ function DataGridCellContent<TData extends object = {}>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<Box
|
<div
|
||||||
ref={cellRef}
|
ref={cellRef}
|
||||||
className={twMerge(
|
className={cn(
|
||||||
'relative grid h-full w-full cursor-default grid-flow-col items-center gap-1',
|
'relative grid w-full cursor-default grid-flow-col items-center gap-1 border-divider px-2 py-1.5 text-primary-text',
|
||||||
isEditable &&
|
isEditable &&
|
||||||
'focus-within:outline-none focus-within:ring-0 focus:ring-0',
|
'focus-within:outline-none focus-within:ring-0 focus:ring-0',
|
||||||
isSelected && 'shadow-outline',
|
isSelected && 'shadow-outline',
|
||||||
isEditing ? 'p-0.5 shadow-outline-dark' : 'px-2 py-1.5',
|
isEditing && 'shadow-outline-dark',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
@@ -324,7 +351,6 @@ function DataGridCellContent<TData extends object = {}>({
|
|||||||
tabIndex={isEditable ? 0 : undefined}
|
tabIndex={isEditable ? 0 : undefined}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
sx={{ backgroundColor: 'transparent' }}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{Children.map(
|
{Children.map(
|
||||||
@@ -338,30 +364,62 @@ function DataGridCellContent<TData extends object = {}>({
|
|||||||
if (!isValidElement(child)) {
|
if (!isValidElement(child)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const { id: rowId } = row.original as { id: string };
|
||||||
return cloneElement(child, {
|
const clonedChild = cloneElement(child, {
|
||||||
...child.props,
|
...child.props,
|
||||||
onSave: handleSave,
|
onSave: handleSave,
|
||||||
optimisticValue,
|
optimisticValue,
|
||||||
onOptimisticValueChange: setOptimisticValue,
|
onOptimisticValueChange: setOptimisticValue,
|
||||||
temporaryValue,
|
temporaryValue,
|
||||||
onTemporaryValueChange: setTemporaryValue,
|
onTemporaryValueChange: setTemporaryValue,
|
||||||
|
rowId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{clonedChild}
|
||||||
|
{id !== 'preview-column' &&
|
||||||
|
type !== 'boolean' &&
|
||||||
|
isNotEmptyValue(optimisticValue) && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const copiableValue =
|
||||||
|
typeof optimisticValue === 'object'
|
||||||
|
? JSON.stringify(optimisticValue)
|
||||||
|
: String(optimisticValue).replace(/\\n/gi, '\n');
|
||||||
|
|
||||||
|
copy(copiableValue, 'Value');
|
||||||
|
}}
|
||||||
|
className="-ml-px h-6 w-6 border-transparent bg-transparent p-1 text-disabled opacity-0 hover:bg-transparent group-hover:opacity-100"
|
||||||
|
aria-label="Copy value"
|
||||||
|
>
|
||||||
|
<Copy width={16} height={16} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
|
// TODO: https://github.com/nhost/nhost/issues/3677
|
||||||
if (isEditable) {
|
if (isEditable) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
disableHoverListener
|
delayDuration={100}
|
||||||
disableFocusListener
|
|
||||||
open={tooltipOpen}
|
open={tooltipOpen}
|
||||||
title={tooltipTitle || ''}
|
onOpenChange={(newState) => {
|
||||||
TransitionProps={{ onExited: resetTooltipTitle }}
|
if (!newState) {
|
||||||
|
resetTooltipTitle();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{content}
|
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||||
|
<TooltipContent>{tooltipTitle}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -370,7 +428,7 @@ function DataGridCellContent<TData extends object = {}>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DataGridCell<TData extends object>(
|
export default function DataGridCell<TData extends object>(
|
||||||
props: DataGridCellProps<TData>,
|
props: PropsWithChildren<DataGridCellProps<TData>>,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<DataGridCellProvider>
|
<DataGridCellProvider>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user