Compare commits

...

35 Commits

Author SHA1 Message Date
David Barroso
afffb887d0 sad 2025-11-17 10:21:48 +01:00
David Barroso
f542e3a57b Merge branch 'heic' of github.com:nhost/nhost into heic 2025-11-17 10:05:50 +01:00
David Barroso
3644a77b2d asd 2025-11-17 10:05:35 +01:00
David Barroso
855b5fc081 Merge branch 'main' into heic 2025-11-17 09:53:28 +01:00
David BM
9dab347348 chore(deps): update js-yaml and validator to address security advisory (#3699) 2025-11-17 09:53:17 +01:00
David Barroso
279d952c4c asd 2025-11-17 09:28:13 +01:00
David Barroso
6c044458ca fix(packages/nhost-js): react native needs special treatment when using FormData (#3697)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-17 08:37:26 +01:00
David Barroso
a7fd13e384 asd 2025-11-16 13:34:56 +01:00
David Barroso
30f9c7498b asd 2025-11-16 13:31:04 +01:00
David Barroso
d015790556 asd 2025-11-16 10:49:17 +01:00
David Barroso
ab6e72ffed asd 2025-11-16 09:40:33 +01:00
David Barroso
03b6fd2d93 asd 2025-11-15 19:50:52 +01:00
David Barroso
fa6d2e6ae1 asd 2025-11-15 19:42:39 +01:00
David Barroso
96c377357e wip 2025-11-15 19:26:50 +01:00
David Barroso
561b108900 asd 2025-11-15 14:47:38 +01:00
David Barroso
7fa7ef2715 asd 2025-11-13 10:55:44 +01:00
github-actions[bot]
5b98f4ece2 release(cli): 1.34.7 (#3695)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-13 10:37:08 +01:00
David Barroso
dc7d3fb4cc chore(cli): bump nhost/dashboard to 2.42.0 (#3693)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-13 10:07:25 +01:00
David Barroso
84ebe77b27 asdqasd 2025-11-13 10:02:24 +01:00
David Barroso
effb2ae17f asd 2025-11-13 09:16:11 +01:00
github-actions[bot]
d15717b67a release(cli): 1.34.6 (#3690)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-13 08:28:30 +01:00
David Barroso
a3c7f89eda fix(cli): mcp: specify items type for arrays in tools (#3687) 2025-11-13 08:23:03 +01:00
github-actions[bot]
6692e0dfc0 release(dashboard): 2.42.0 (#3688)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-13 08:21:19 +01:00
David Barroso
8541165781 chore(cli): update bindings (#3689) 2025-11-12 12:29:31 +01:00
robertkasza
5cd5ebbc65 feat(dashboard): datatable design improvements (#3657) 2025-11-12 12:02:30 +01:00
robertkasza
5066ef708a chore(dashboard): remove v2 ui components from datatable (#3568) 2025-11-12 11:26:32 +01:00
github-actions[bot]
a6a378c5a6 release(services/auth): 0.43.1 (#3682)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-11 12:58:31 +01:00
David Barroso
a3a3cf205d fix(auth): return meaningful error if the provider's account is already linked (#3680) 2025-11-11 12:56:59 +01:00
dependabot[bot]
3fd2e63db3 chore(ci): bump Codium-ai/pr-agent from 0.30 to 0.31 (#3676)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 08:56:17 +01:00
github-actions[bot]
f5956f1b2e release(cli): 1.34.5 (#3655)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-06 15:34:31 +01:00
David Barroso
f3b397b0d8 chore(cli): bump nhost/dashboard to 2.41.0 (#3669)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-06 13:42:32 +01:00
David Barroso
b7940087ee chore(cli): udpate certs and schema (#3675) 2025-11-06 13:04:09 +01:00
github-actions[bot]
3dae655858 release(services/storage): 0.9.1 (#3673)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-06 11:42:33 +01:00
David Barroso
2aa269734b fix(storage): format date-time headers with RFC2822 (#3672) 2025-11-06 11:40:11 +01:00
David Barroso
bc91836f83 chore(ci): replace the correct string on .github/workflows/dashboard_wf_release.yaml (#3670)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 10:26:16 +01:00
306 changed files with 30389 additions and 173275 deletions

View File

@@ -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

View File

@@ -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 }}

View File

@@ -3,7 +3,6 @@
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json", "$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"moderate": true, "moderate": true,
"allowlist": [ "allowlist": [
"GHSA-9965-vmph-33xx", // https://github.com/advisories/GHSA-9965-vmph-33xx Update package once have a fix
"GHSA-7mvr-c777-76hp" // https://github.com/advisories/GHSA-7mvr-c777-76hp Update package once Nix side is also updated "GHSA-7mvr-c777-76hp" // https://github.com/advisories/GHSA-7mvr-c777-76hp Update package once Nix side is also updated
] ]
} }

View File

@@ -1,3 +1,28 @@
## [cli@1.34.7] - 2025-11-13
### ⚙️ Miscellaneous Tasks
- *(cli)* Bump nhost/dashboard to 2.42.0 (#3693)
## [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.

View 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.42.0",
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"), Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
}, },
&cli.StringFlag{ //nolint:exhaustruct &cli.StringFlag{ //nolint:exhaustruct

View File

@@ -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.42.0",
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"), Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
}, },
&cli.StringFlag{ //nolint:exhaustruct &cli.StringFlag{ //nolint:exhaustruct

View File

@@ -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),

View File

@@ -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",

View File

@@ -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

View File

@@ -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"),
), ),
) )

View File

@@ -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"`

View File

@@ -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

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -1,3 +1,14 @@
## [@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 ## [@nhost/dashboard@2.41.0] - 2025-11-04
### 🚀 Features ### 🚀 Features

View 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();

View File

@@ -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');

View File

@@ -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",
@@ -125,7 +126,7 @@
"timezones-list": "^3.1.0", "timezones-list": "^3.1.0",
"utility-types": "^3.11.0", "utility-types": "^3.11.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"validator": "^13.11.0", "validator": "^13.15.20",
"yup": "^1.4.0", "yup": "^1.4.0",
"yup-password": "^0.2.2", "yup-password": "^0.2.2",
"zod": "^3.23.8" "zod": "^3.23.8"
@@ -231,7 +232,8 @@
} }
}, },
"overrides": { "overrides": {
"esbuild@<=0.24.2": ">=0.25.0" "esbuild@<=0.24.2": ">=0.25.0",
"js-yaml@<=4.1.0": ">=4.1.1"
} }
} }
} }

170
dashboard/pnpm-lock.yaml generated
View File

@@ -6,6 +6,7 @@ settings:
overrides: overrides:
esbuild@<=0.24.2: '>=0.25.0' esbuild@<=0.24.2: '>=0.25.0'
js-yaml@<=4.1.0: '>=4.1.1'
packageExtensionsChecksum: sha256-gRFeykwiwMfEE6etcYx6N48XwVeKzxbqNveL7KTQgSQ= packageExtensionsChecksum: sha256-gRFeykwiwMfEE6etcYx6N48XwVeKzxbqNveL7KTQgSQ=
@@ -55,6 +56,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 +126,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 +225,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)
@@ -317,8 +321,8 @@ importers:
specifier: ^9.0.1 specifier: ^9.0.1
version: 9.0.1 version: 9.0.1
validator: validator:
specifier: ^13.11.0 specifier: ^13.15.20
version: 13.12.0 version: 13.15.23
yup: yup:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.5.0 version: 1.5.0
@@ -2063,6 +2067,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 +4058,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 +4977,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==}
@@ -6322,8 +6338,8 @@ packages:
js-tokens@9.0.1: js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.1.0: js-yaml@4.1.1:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true hasBin: true
jsdom@22.1.0: jsdom@22.1.0:
@@ -6547,8 +6563,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 +7423,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 +7500,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 +7629,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 +8240,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==}
@@ -8558,8 +8595,8 @@ packages:
v8-compile-cache-lib@3.0.1: v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
validator@13.12.0: validator@13.15.23:
resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
vfile-message@4.0.2: vfile-message@4.0.2:
@@ -8576,7 +8613,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
@@ -8900,7 +8937,7 @@ snapshots:
dependencies: dependencies:
'@jsdevtools/ono': 7.1.3 '@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
js-yaml: 4.1.0 js-yaml: 4.1.1
'@apidevtools/openapi-schemas@2.1.0': {} '@apidevtools/openapi-schemas@2.1.0': {}
@@ -8946,7 +8983,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 +10131,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
@@ -10299,7 +10336,7 @@ snapshots:
globals: 13.24.0 globals: 13.24.0
ignore: 5.3.2 ignore: 5.3.2
import-fresh: 3.3.0 import-fresh: 3.3.0
js-yaml: 4.1.0 js-yaml: 4.1.1
minimatch: 3.1.2 minimatch: 3.1.2
strip-json-comments: 3.1.1 strip-json-comments: 3.1.1
transitivePeerDependencies: transitivePeerDependencies:
@@ -10934,6 +10971,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
@@ -10971,7 +11020,7 @@ snapshots:
loglevel: 1.9.2 loglevel: 1.9.2
loglevel-plugin-prefix: 0.8.4 loglevel-plugin-prefix: 0.8.4
minimatch: 6.2.0 minimatch: 6.2.0
validator: 13.12.0 validator: 13.15.23
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
@@ -11273,7 +11322,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 +11359,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 +11368,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 +11399,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 +11620,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 +11747,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 +11772,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 +11797,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 +11848,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 +11903,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 +11922,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 +11973,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 +12082,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 +12112,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 +12143,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 +12306,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 +12393,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 +12406,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 +12436,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 +12451,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 +13090,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 +13794,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 +13885,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:
@@ -14148,7 +14199,7 @@ snapshots:
cosmiconfig@8.3.6(typescript@5.8.3): cosmiconfig@8.3.6(typescript@5.8.3):
dependencies: dependencies:
import-fresh: 3.3.1 import-fresh: 3.3.1
js-yaml: 4.1.0 js-yaml: 4.1.1
parse-json: 5.2.0 parse-json: 5.2.0
path-type: 4.0.0 path-type: 4.0.0
optionalDependencies: optionalDependencies:
@@ -14158,7 +14209,7 @@ snapshots:
dependencies: dependencies:
env-paths: 2.2.1 env-paths: 2.2.1
import-fresh: 3.3.1 import-fresh: 3.3.1
js-yaml: 4.1.0 js-yaml: 4.1.1
parse-json: 5.2.0 parse-json: 5.2.0
optionalDependencies: optionalDependencies:
typescript: 5.8.3 typescript: 5.8.3
@@ -14183,6 +14234,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 +14409,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:
@@ -15008,7 +15063,7 @@ snapshots:
imurmurhash: 0.1.4 imurmurhash: 0.1.4
is-glob: 4.0.3 is-glob: 4.0.3
is-path-inside: 3.0.3 is-path-inside: 3.0.3
js-yaml: 4.1.0 js-yaml: 4.1.1
json-stable-stringify-without-jsonify: 1.0.1 json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1 levn: 0.4.1
lodash.merge: 4.6.2 lodash.merge: 4.6.2
@@ -15919,7 +15974,7 @@ snapshots:
js-tokens@9.0.1: {} js-tokens@9.0.1: {}
js-yaml@4.1.0: js-yaml@4.1.1:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
@@ -16162,7 +16217,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 +17257,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 +17334,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 +17405,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 +17461,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 +17497,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 +17545,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 +18190,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: {}
@@ -18466,7 +18536,7 @@ snapshots:
v8-compile-cache-lib@3.0.1: {} v8-compile-cache-lib@3.0.1: {}
validator@13.12.0: {} validator@13.15.23: {}
vfile-message@4.0.2: vfile-message@4.0.2:
dependencies: dependencies:

View File

@@ -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> {}

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
export { default as DragAndDropList } from './DragAndDropList';
export * from './DraggableItem';
export { default as DraggableItem } from './DraggableItem';

View File

@@ -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}

View File

@@ -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>
); );
} }

View File

@@ -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;

View 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;

View File

@@ -0,0 +1 @@
export { default as FormSelect } from './FormSelect';

View 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;

View File

@@ -0,0 +1 @@
export { default as FormTextarea } from './FormTextarea';

View File

@@ -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">

View File

@@ -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',

View File

@@ -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>
); );
} }

View File

@@ -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;

View File

@@ -1 +0,0 @@
export { default as KeyIcon } from './KeyIcon';

View File

@@ -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',

View 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,
}

View File

@@ -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: {

View File

@@ -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>
)); ));

View File

@@ -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>

View File

@@ -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}

View File

@@ -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,
)} )}

View File

@@ -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)}

View File

@@ -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,

View File

@@ -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}

View File

@@ -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"

View File

@@ -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}

View File

@@ -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}

View File

@@ -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

View File

@@ -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} />

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1 @@
export { default as ColumnCustomizer } from './ColumnCustomizer';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -0,0 +1 @@
export { default as DataGridCustomizerControls } from './DataGridCustomizerControls';

View File

@@ -0,0 +1 @@
export type RowDensity = 'comfortable' | 'compact';

View File

@@ -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>
); );
} }

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>
); );
} }

View File

@@ -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}
/> />
), ),
}); });

View File

@@ -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>
); );
} }

View File

@@ -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>
</> </>
); );
} }

View File

@@ -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;

View File

@@ -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>
); );
} }

View File

@@ -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>
); );

View File

@@ -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}

View File

@@ -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]);

View File

@@ -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;
} }
/** /**

View File

@@ -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.

View File

@@ -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)

View File

@@ -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;

View File

@@ -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();
});
}); });

View File

@@ -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>
); );
} }

View File

@@ -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}
/> />
); );
}, },

View File

@@ -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()}

View File

@@ -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} />
); );
} }

View File

@@ -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>

View File

@@ -1,23 +1,12 @@
import { Input, inputClasses } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v3/input';
import type { TextProps } from '@/components/ui/v2/Text';
import { Text } from '@/components/ui/v2/Text';
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 { cn } from '@/lib/utils';
import { getDateComponents } from '@/utils/getDateComponents'; import { getDateComponents } from '@/utils/getDateComponents';
import type { ChangeEvent, KeyboardEvent } from 'react'; import type { ChangeEvent, KeyboardEvent, Ref } from 'react';
import { twMerge } from 'tailwind-merge';
export interface DataGridDateCellProps<TData extends object> export interface DataGridDateCellProps<TData extends object>
extends CommonDataGridCellProps<TData, string> { extends CommonDataGridCellProps<TData, string> {}
/**
* Props to be passed to date display.
*/
dateProps?: TextProps;
/**
* Props to be passed to time display.
*/
timeProps?: TextProps;
}
export default function DataGridDateCell<TData extends object>({ export default function DataGridDateCell<TData extends object>({
onSave, onSave,
@@ -27,13 +16,8 @@ export default function DataGridDateCell<TData extends object>({
cell: { cell: {
column: { specificType }, column: { specificType },
}, },
dateProps,
timeProps,
className, className,
}: DataGridDateCellProps<TData>) { }: DataGridDateCellProps<TData>) {
const { className: dateClassName, ...restDateProps } = dateProps || {};
const { className: timeClassName, ...restTimeProps } = timeProps || {};
// Note: No date (year-month-day) is saved for time / timetz columns, so we // Note: No date (year-month-day) is saved for time / timetz columns, so we
// need to add it manually. // need to add it manually.
const date = const date =
@@ -51,8 +35,7 @@ export default function DataGridDateCell<TData extends object>({
), ),
}); });
const { inputRef, focusCell, isEditing, cancelEditCell } = const { inputRef, isEditing } = useDataGridCell<HTMLInputElement>();
useDataGridCell<HTMLInputElement>();
async function handleSave() { async function handleSave() {
if (onSave) { if (onSave) {
@@ -77,8 +60,6 @@ export default function DataGridDateCell<TData extends object>({
if (event.key === 'Enter') { if (event.key === 'Enter') {
await handleSave(); await handleSave();
await focusCell();
cancelEditCell();
} }
} }
@@ -91,78 +72,38 @@ export default function DataGridDateCell<TData extends object>({
if (isEditing) { if (isEditing) {
return ( return (
<Input <Input
ref={inputRef} ref={inputRef as Ref<HTMLInputElement>}
value={ value={
temporaryValue !== null && typeof temporaryValue !== 'undefined' temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue ? temporaryValue
: '' : ''
} }
onKeyDown={handleKeyDown}
onChange={handleChange} onChange={handleChange}
fullWidth onKeyDown={handleKeyDown}
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch" wrapperClassName="absolute top-0 z-10 w-full top-0 left-0 h-full"
sx={{ className="h-full w-full resize-none rounded-none border-none px-2 py-1.5 !text-xs outline-none focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200"
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/> />
); );
} }
if (!optimisticValue) { if (!optimisticValue) {
return ( return <p className="truncate text-xs text-secondary">null</p>;
<Text className="truncate text-xs" color="secondary">
null
</Text>
);
} }
if (specificType === 'interval') { if (specificType === 'interval') {
return <Text className="truncate text-xs">{optimisticValue}</Text>; return <p className="truncate text-xs">{optimisticValue}</p>;
} }
return ( return (
<div className={twMerge('grid grid-flow-row', className)}> <div className={cn('grid grid-flow-row', className)}>
{specificType !== 'time' && specificType !== 'timetz' && ( <p className="truncate text-xs">
<Text {specificType !== 'time' && specificType !== 'timetz' && (
className={twMerge('truncate text-xs', dateClassName)} <span>{[year, month, day].filter(Boolean).join('-')}</span>
{...restDateProps} )}{' '}
> {specificType !== 'date' && (
{[year, month, day].filter(Boolean).join('-')} <span>{[hour, minute, second].filter(Boolean).join(':')}</span>
</Text> )}
)} </p>
{specificType !== 'date' && (
<Text
className={twMerge('truncate text-xs', timeClassName)}
color={
specificType === 'time' || specificType === 'timetz'
? 'primary'
: 'secondary'
}
{...restTimeProps}
>
{[hour, minute, second].filter(Boolean).join(':')}
</Text>
)}
</div> </div>
); );
} }

View File

@@ -1,108 +0,0 @@
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import type { ChangeEvent, KeyboardEvent } from 'react';
export type DataGridDecimalCellProps<TData extends object> =
CommonDataGridCellProps<TData, number | string | null>;
export default function DataGridDecimalCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
}: DataGridDecimalCellProps<TData>) {
const { inputRef, focusCell, isEditing, cancelEditCell } =
useDataGridCell<HTMLInputElement>();
async function handleSave() {
if (onSave) {
if (typeof temporaryValue === 'string') {
await onSave(parseFloat(temporaryValue));
} else if (typeof temporaryValue === 'number') {
await onSave(temporaryValue);
} else {
await onSave(null);
}
}
}
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
if (event.key === 'Tab') {
await handleSave();
}
if (event.key === 'Enter') {
await handleSave();
await focusCell();
cancelEditCell();
}
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
if (onTemporaryValueChange) {
onTemporaryValueChange(event.target.value ?? null);
}
}
if (isEditing) {
return (
<Input
type="text"
ref={inputRef}
value={
temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue
: ''
}
onKeyDown={handleKeyDown}
onChange={handleChange}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/>
);
}
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
return (
<Text className="truncate !text-xs" color="disabled">
null
</Text>
);
}
return <Text className="truncate !text-xs">{optimisticValue}</Text>;
}

View File

@@ -1,2 +0,0 @@
export * from './DataGridDecimalCell';
export { default as DataGridDecimalCell } from './DataGridDecimalCell';

View File

@@ -2,9 +2,7 @@ import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/da
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider'; import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
import { DataGridHeaderButton } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeaderButton'; import { DataGridHeaderButton } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeaderButton';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTheme } from '@mui/material';
import type { DetailedHTMLProps, HTMLProps } from 'react'; import type { DetailedHTMLProps, HTMLProps } from 'react';
import { twMerge } from 'tailwind-merge';
export interface HeaderActionProps export interface HeaderActionProps
extends DetailedHTMLProps<HTMLProps<HTMLElement>, HTMLElement> {} extends DetailedHTMLProps<HTMLProps<HTMLElement>, HTMLElement> {}
@@ -22,55 +20,54 @@ export default function DataGridHeader({
...props ...props
}: DataGridHeaderProps) { }: DataGridHeaderProps) {
const { flatHeaders } = useDataGridConfig(); const { flatHeaders } = useDataGridConfig();
const theme = useTheme();
return ( return (
<div <div
className={twMerge( className={cn(
'sticky top-0 z-30 inline-flex w-full items-center', 'sticky top-0 z-30 inline-flex w-full items-center',
className, className,
)} )}
{...props} {...props}
> >
{flatHeaders.map((column: DataBrowserGridColumn) => { {flatHeaders
const sortByProps = column.getSortByToggleProps(); .filter(({ isVisible }) => isVisible)
const headerProps = column.getHeaderProps({ .map((column: DataBrowserGridColumn) => {
style: { display: 'inline-flex' }, const sortByProps = column.getSortByToggleProps();
...sortByProps, const headerProps = column.getHeaderProps({
}); style: { display: 'inline-flex' },
...sortByProps,
});
return ( return (
<div <div
className={cn( className={cn(
'group relative inline-flex self-stretch overflow-hidden font-display text-xs font-bold focus:outline-none focus-visible:outline-none', 'group relative inline-flex self-stretch overflow-hidden font-display text-xs font-bold focus:outline-none focus-visible:outline-none',
'border-b-1 border-r-1', 'border-b-1 border-r-1',
{ 'sticky left-0 max-w-2': column.id === 'selection-column' }, 'bg-paper',
'dark:text-[#dfecf5]', { 'sticky left-0 max-w-2': column.id === 'selection-column' },
)} )}
style={{ style={{
...headerProps.style, ...headerProps.style,
backgroundColor: column.isDisabled maxWidth:
? theme.palette.background.default column.id === 'selection-column'
: theme.palette.background.paper, ? 32
maxWidth: : headerProps.style?.maxWidth,
column.id === 'selection-column' width:
? 32 column.id === 'selection-column'
: headerProps.style?.maxWidth, ? '100%'
width: : headerProps.style?.width,
column.id === 'selection-column' zIndex:
? '100%' column.id === 'selection-column'
: headerProps.style?.width, ? 10
zIndex: : headerProps.style?.zIndex,
column.id === 'selection-column' position: undefined,
? 10 }}
: headerProps.style?.zIndex, key={column.id}
position: undefined, >
}} <DataGridHeaderButton column={column} headerProps={headerProps} />
key={column.id} </div>
> );
<DataGridHeaderButton column={column} headerProps={headerProps} /> })}
</div>
);
})}
</div> </div>
); );
} }

View File

@@ -1,9 +1,9 @@
import { Button } from '@/components/ui/v3/button'; import { Button } from '@/components/ui/v3/button';
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 { cn } from '@/lib/utils';
import { ArrowDown, ArrowUp } from 'lucide-react'; import { ArrowDown, ArrowUp } from 'lucide-react';
import type { TableHeaderProps } from 'react-table'; import type { TableHeaderProps } from 'react-table';
import { twMerge } from 'tailwind-merge';
interface DataGridHeaderButtonProps<T extends object> { interface DataGridHeaderButtonProps<T extends object> {
column: DataBrowserGridColumn<T>; column: DataBrowserGridColumn<T>;
@@ -38,7 +38,7 @@ export default function DataGridHeaderButton<T extends object>({
return ( return (
<Button <Button
variant="ghost" variant="ghost"
className={twMerge( className={cn(
'h-fit p-0 text-xs focus:outline-none motion-safe:transition-colors dark:hover:bg-[#21262d]', 'h-fit p-0 text-xs focus:outline-none motion-safe:transition-colors dark:hover:bg-[#21262d]',
)} )}
disabled={column.isDisabled || column.disableSortBy} disabled={column.isDisabled || column.disableSortBy}
@@ -65,7 +65,7 @@ export default function DataGridHeaderButton<T extends object>({
{...column.getResizerProps({ {...column.getResizerProps({
onClick: (event: Event) => event.stopPropagation(), onClick: (event: Event) => event.stopPropagation(),
})} })}
className="absolute -right-0.5 bottom-0 top-0 z-10 h-full w-1.5 group-hover:bg-slate-900 group-hover:bg-opacity-20 group-active:bg-slate-900 group-active:bg-opacity-20 motion-safe:transition-colors" className="absolute -right-0.5 bottom-0 top-0 z-10 h-full w-2 group-hover:bg-slate-900 group-hover:bg-opacity-20 group-active:bg-slate-900 group-active:bg-opacity-20 motion-safe:transition-colors"
/> />
)} )}
</Button> </Button>

View File

@@ -1,112 +0,0 @@
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import {
useDataGridCell,
type CommonDataGridCellProps,
} from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import type { ChangeEvent, KeyboardEvent } from 'react';
export type DataGridIntegerCellProps<TData extends object> =
CommonDataGridCellProps<TData, number | null>;
export default function DataGridIntegerCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
}: DataGridIntegerCellProps<TData>) {
const { inputRef, focusCell, isEditing, cancelEditCell } =
useDataGridCell<HTMLInputElement>();
async function handleSave() {
if (onSave) {
if (typeof temporaryValue === 'number') {
await onSave(temporaryValue);
} else {
await onSave(null);
}
}
}
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
if (event.key === 'Tab') {
await handleSave();
}
if (event.key === 'Enter') {
await handleSave();
await focusCell();
cancelEditCell();
}
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
if (onTemporaryValueChange) {
if (event.target.value) {
onTemporaryValueChange(parseInt(event.target.value, 10));
} else {
onTemporaryValueChange(null);
}
}
}
if (isEditing) {
return (
<Input
type="number"
ref={inputRef}
value={
temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue
: ''
}
onKeyDown={handleKeyDown}
onChange={handleChange}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/>
);
}
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
return (
<Text className="truncate !text-xs" color="disabled">
null
</Text>
);
}
return <Text className="truncate !text-xs">{optimisticValue}</Text>;
}

View File

@@ -1,2 +0,0 @@
export * from './DataGridIntegerCell';
export { default as DataGridIntegerCell } from './DataGridIntegerCell';

View File

@@ -0,0 +1,97 @@
import { Input } from '@/components/ui/v3/input';
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { isNotEmptyValue } from '@/lib/utils';
import {
useEffect,
type ChangeEvent,
type KeyboardEvent,
type Ref,
} from 'react';
export type DataGridNumericCellProps<TData extends object> =
CommonDataGridCellProps<TData, number | null>;
export default function DataGridNumericCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
cell: {
column: { dataType },
},
}: DataGridNumericCellProps<TData>) {
const { inputRef, isEditing } = useDataGridCell<HTMLInputElement>();
useEffect(() => {
const controller = new AbortController();
function preventWheelInNumberInput(event: WheelEvent) {
event.preventDefault();
}
if (inputRef.current) {
inputRef.current.addEventListener('wheel', preventWheelInNumberInput, {
passive: false,
signal: controller.signal,
});
}
return () => controller.abort();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputRef.current]);
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
if (isNotEmptyValue(onSave) && typeof temporaryValue !== 'undefined') {
if (event.key === 'Tab') {
await onSave?.(temporaryValue);
}
if (event.key === 'Enter') {
await onSave?.(temporaryValue);
}
}
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
if (onTemporaryValueChange) {
const newValue = isNotEmptyValue(event.target.value)
? +event.target.value
: null;
onTemporaryValueChange(newValue);
}
}
if (isEditing) {
const step = dataType === 'integer' ? 1 : 0.1;
return (
<Input
type="number"
ref={inputRef as Ref<HTMLInputElement>}
value={
temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue
: ''
}
onChange={handleChange}
onKeyDown={handleKeyDown}
wrapperClassName="absolute top-0 z-10 w-full top-0 left-0 h-full"
className="h-full w-full resize-none rounded-none border-none px-2 py-1.5 !text-xs outline-none [appearance:textfield] focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
step={step}
/>
);
}
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
return <p className="truncate !text-xs text-disabled">null</p>;
}
return <p className="truncate !text-xs">{optimisticValue}</p>;
}

View File

@@ -0,0 +1,2 @@
export * from './DataGridNumericCell';
export { default as DataGridNumericCell } from './DataGridNumericCell';

View File

@@ -1,13 +1,8 @@
import type { BoxProps } from '@/components/ui/v2/Box'; import { Button, type ButtonProps } from '@/components/ui/v3/button';
import { Box } from '@/components/ui/v2/Box'; import { cn } from '@/lib/utils';
import type { IconButtonProps } from '@/components/ui/v2/IconButton'; import { ChevronLeft, ChevronRight } from 'lucide-react';
import { IconButton } from '@/components/ui/v2/IconButton';
import { ChevronLeftIcon } from '@/components/ui/v2/icons/ChevronLeftIcon';
import { ChevronRightIcon } from '@/components/ui/v2/icons/ChevronRightIcon';
import { Text } from '@/components/ui/v2/Text';
import clsx from 'clsx';
export interface DataGridPaginationProps extends BoxProps { export interface DataGridPaginationProps {
/** /**
* Number of pages. * Number of pages.
*/ */
@@ -27,11 +22,12 @@ export interface DataGridPaginationProps extends BoxProps {
/** /**
* Props to be passed to the next button component. * Props to be passed to the next button component.
*/ */
nextButtonProps?: IconButtonProps; nextButtonProps?: ButtonProps;
/** /**
* Props to be passed to the previous button component. * Props to be passed to the previous button component.
*/ */
prevButtonProps?: IconButtonProps; prevButtonProps?: ButtonProps;
className?: string;
} }
export default function DataGridPagination({ export default function DataGridPagination({
@@ -42,50 +38,48 @@ export default function DataGridPagination({
onOpenNextPage, onOpenNextPage,
nextButtonProps, nextButtonProps,
prevButtonProps, prevButtonProps,
...props
}: DataGridPaginationProps) { }: DataGridPaginationProps) {
return ( return (
<Box <div
className={clsx( className={cn(
'grid grid-flow-col items-center justify-around rounded-md border-1', 'grid grid-flow-col items-center justify-around rounded-md border-1 px-1',
className, className,
)} )}
{...props}
> >
<IconButton <Button
variant="borderless" variant="outline"
color="secondary" size="icon"
disabled={currentPage === 1} disabled={currentPage === 1}
onClick={onOpenPrevPage} onClick={onOpenPrevPage}
aria-label="Previous page" aria-label="Previous page"
className="h-max w-max border-none bg-transparent dark:hover:bg-[#2f363d]"
{...prevButtonProps} {...prevButtonProps}
> >
<ChevronLeftIcon className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
</IconButton> </Button>
<span <span
className={clsx( className={cn(
'mx-1 inline-block font-display font-medium', 'mx-1 inline-block font-display font-medium',
currentPage > 99 ? 'text-xs' : 'text-sm+', currentPage > 99 ? 'text-xs' : 'text-sm+',
)} )}
> >
{currentPage} {currentPage}
<Text component="span" className="mx-1 inline-block" color="disabled"> <span className="mx-1 inline-block text-disabled">/</span>
/
</Text>
{totalPages} {totalPages}
</span> </span>
<IconButton <Button
variant="borderless" variant="outline"
color="secondary" size="icon"
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
onClick={onOpenNextPage} onClick={onOpenNextPage}
aria-label="Next page" aria-label="Next page"
className="h-max w-max border-none bg-transparent dark:hover:bg-[#2f363d]"
{...nextButtonProps} {...nextButtonProps}
> >
<ChevronRightIcon className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
</IconButton> </Button>
</Box> </div>
); );
} }

View File

@@ -1,17 +1,20 @@
import { Modal } from '@/components/ui/v1/Modal';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { IconButton } from '@/components/ui/v2/IconButton';
import { AudioPreviewIcon } from '@/components/ui/v2/icons/AudioPreviewIcon'; import { AudioPreviewIcon } from '@/components/ui/v2/icons/AudioPreviewIcon';
import { FilePreviewIcon } from '@/components/ui/v2/icons/FilePreviewIcon';
import { PDFPreviewIcon } from '@/components/ui/v2/icons/PDFPreviewIcon'; import { PDFPreviewIcon } from '@/components/ui/v2/icons/PDFPreviewIcon';
import { VideoPreviewIcon } from '@/components/ui/v2/icons/VideoPreviewIcon'; import { VideoPreviewIcon } from '@/components/ui/v2/icons/VideoPreviewIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon'; import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from '@/components/ui/v3/dialog';
import { Spinner } from '@/components/ui/v3/spinner';
import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient'; import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
import { useProject } from '@/features/orgs/projects/hooks/useProject'; import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { usePreviewToggle } from '@/features/orgs/projects/storage/dataGrid/hooks/usePreviewToggle'; import { usePreviewToggle } from '@/features/orgs/projects/storage/dataGrid/hooks/usePreviewToggle';
import { cn } from '@/lib/utils';
import { getHasuraAdminSecret } from '@/utils/env'; import { getHasuraAdminSecret } from '@/utils/env';
import clsx from 'clsx'; import { FileText } from 'lucide-react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useEffect, useReducer, useState } from 'react'; import { useEffect, useReducer, useState } from 'react';
import type { CellProps } from 'react-table'; import type { CellProps } from 'react-table';
@@ -246,124 +249,119 @@ export default function DataGridPreviewCell<TData extends object>({
} }
} }
function handleClose(openState: boolean) {
if (!openState) {
setShowModal(false);
dispatch({ type: 'CLEAR_PREVIEW' });
}
}
if (loading) { if (loading) {
return <ActivityIndicator delay={500} className="mx-auto" />; return (
<div className="flex w-full justify-center">
<Spinner className="mx-auto h-4 w-4" />
</div>
);
} }
if (error) { if (error) {
return ( return (
<Box <div className="box grid w-full grid-flow-col items-center justify-center gap-1 text-center !text-error-main">
className="grid w-full grid-flow-col items-center justify-center gap-1 text-center" <FileText className="text-error-main" /> Error
sx={{ color: 'error.main' }} </div>
>
<FilePreviewIcon error /> Error
</Box>
); );
} }
return ( return (
<> <Dialog open={showModal} onOpenChange={handleClose}>
<Modal <DialogTrigger asChild>
wrapperClassName="items-center" <button
showModal={showModal} type="button"
close={() => setShowModal(false)} aria-label={alt}
afterLeave={() => dispatch({ type: 'CLEAR_PREVIEW' })} onClick={handleOpenPreview}
className={clsx( className={cn('flex h-full w-full items-center justify-center')}
previewableImages.includes(mimeType) || isVideo || isAudio
? 'mx-12 flex h-screen items-center justify-center'
: 'mt-4 inline-block h-near-screen w-full px-12',
)}
>
<Box
className={clsx(
!isJson && 'bg-checker-pattern',
'relative mx-auto flex overflow-hidden rounded-md',
)}
sx={{
backgroundColor: isJson ? 'background.default' : undefined,
color: 'text.primary',
}}
> >
{!previewLoading && ( {previewEnabled &&
<IconButton previewableImages.includes(mimeType) &&
aria-label="Close" objectUrl ? (
variant="borderless" <picture className="h-full w-20">
color="secondary" <source srcSet={objectUrl} type={mimeType} />
className="absolute right-2 top-2 z-50 p-2"
sx={{
[`&:hover, &:active, &:focus`]: {
backgroundColor: (theme) => {
if (isAudio || isVideo || isJson) {
return 'common.black';
}
return theme.palette.mode === 'dark'
? 'grey.800'
: 'grey.200';
},
},
}}
onClick={() => setShowModal(false)}
>
<XIcon
className="h-5 w-5"
sx={{
color: (theme) => {
if (isAudio || isVideo || isJson) {
return 'common.white';
}
return theme.palette.mode === 'dark'
? 'grey.100'
: 'grey.700';
},
}}
/>
</IconButton>
)}
{previewLoading && !previewUrl && (
<ActivityIndicator
delay={500}
className="mx-auto"
label="Loading preview..."
/>
)}
{previewError && (
<Box
className="px-6 py-3.5 pr-12 text-start font-medium"
sx={{ color: 'error.main' }}
>
<p>Error: Preview can&apos;t be loaded.</p>
<p>{previewError.message}</p>
</Box>
)}
{previewUrl && isImage && (
<picture className="h-auto max-h-near-screen min-h-38 min-w-38">
<source srcSet={previewUrl} type={mimeType} />
<img <img
src={previewUrl} src={objectUrl}
alt={alt} alt={alt}
className="h-full w-full object-scale-down" className="h-full w-full object-scale-down"
/> />
</picture> </picture>
)} ) : (
<>
{isVideo && <VideoPreviewIcon className="h-5 w-5" />}
{isAudio && <AudioPreviewIcon className="h-5 w-5" />}
{mimeType === 'application/pdf' && (
<PDFPreviewIcon className="h-5 w-5" />
)}
{!isVideo &&
!isAudio &&
mimeType !== 'application/pdf' &&
fallbackPreview}
</>
)}
</button>
</DialogTrigger>
<DialogContent
closeButtonClassName={cn({ 'text-white': isVideo || isAudio })}
className={cn(
{ 'bg-checker-pattern': !isJson && !previewError },
{ 'p-0': isVideo || isAudio },
isAudio ? '!w-auto' : 'h-[90vh] min-w-[96vw]',
'flex items-center justify-center overflow-hidden rounded-md',
)}
>
<>
<DialogTitle className="hidden">{alt}</DialogTitle>
<DialogDescription className="hidden">{alt}</DialogDescription>
{previewLoading && !previewUrl && (
<Spinner
className={cn('h-5 w-5', {
'!stroke-[#1e324b]': !isJson,
})}
wrapperClassName={cn('flex-row gap-1 text-xs', {
'text-disabled': isJson,
'text-gray-600': !isJson,
})}
>
Loading preview...
</Spinner>
)}
{previewError && (
<div className="px-6 py-3.5 pr-12 text-start font-medium !text-error-main">
<p>Error: Preview can&apos;t be loaded.</p>
<p>{previewError?.message}</p>
</div>
)}
{previewUrl && isImage && (
<picture className="flex h-full max-h-full items-center justify-center">
<source srcSet={previewUrl} type={mimeType} />
<img
src={previewUrl}
alt={alt}
className="h-full max-w-full object-contain"
/>
</picture>
)}
{previewUrl && isVideo && ( {previewUrl && isVideo && (
<video <video
autoPlay autoPlay
controls controls
className="h-auto max-h-near-screen w-full bg-black" className="h-full w-full rounded-sm bg-black"
> >
<track kind="captions" /> <track kind="captions" />
<source src={previewUrl} type={mimeType} /> <source src={previewUrl} type={mimeType} />
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
)} )}
{previewUrl && isAudio && ( {previewUrl && isAudio && (
<audio autoPlay controls className="h-28 bg-black"> <audio autoPlay controls className="h-28 bg-black">
<track kind="captions" /> <track kind="captions" />
@@ -371,7 +369,6 @@ export default function DataGridPreviewCell<TData extends object>({
Your browser does not support the audio tag. Your browser does not support the audio tag.
</audio> </audio>
)} )}
{!previewLoading && {!previewLoading &&
previewUrl && previewUrl &&
!previewableImages.includes(mimeType) && !previewableImages.includes(mimeType) &&
@@ -383,52 +380,8 @@ export default function DataGridPreviewCell<TData extends object>({
title="File preview" title="File preview"
/> />
)} )}
</Box> </>
</Modal> </DialogContent>
</Dialog>
<div className="flex h-full w-full justify-center">
{previewEnabled && previewableImages.includes(mimeType) && objectUrl ? (
<button
type="button"
aria-label={alt}
onClick={handleOpenPreview}
className="mx-auto h-full"
>
<picture className="h-full w-20">
<source srcSet={objectUrl} type={mimeType} />
<img
src={objectUrl}
alt={alt}
className="h-full w-full object-scale-down"
/>
</picture>
</button>
) : null}
{(!previewableImages.includes(mimeType) ||
!objectUrl ||
!previewEnabled) && (
<button
type="button"
onClick={handleOpenPreview}
aria-label={alt}
className="grid h-full w-full items-center justify-center self-center"
>
{isVideo && <VideoPreviewIcon className="h-5 w-5" />}
{isAudio && <AudioPreviewIcon className="h-5 w-5" />}
{mimeType === 'application/pdf' && (
<PDFPreviewIcon className="h-5 w-5" />
)}
{!isVideo &&
!isAudio &&
mimeType !== 'application/pdf' &&
fallbackPreview}
</button>
)}
</div>
</>
); );
} }

View File

@@ -1,12 +1,10 @@
import { Button } from '@/components/ui/v2/Button'; import { Input } from '@/components/ui/v3/input';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon'; import { Textarea } from '@/components/ui/v3/textarea';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { import {
useDataGridCell, useDataGridCell,
type CommonDataGridCellProps, type CommonDataGridCellProps,
} from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell'; } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { copy } from '@/utils/copy';
import type { ChangeEvent, KeyboardEvent, Ref } from 'react'; import type { ChangeEvent, KeyboardEvent, Ref } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -19,7 +17,7 @@ export default function DataGridTextCell<TData extends object>({
temporaryValue, temporaryValue,
onTemporaryValueChange, onTemporaryValueChange,
cell: { cell: {
column: { isCopiable, specificType }, column: { specificType },
}, },
}: DataGridTextCellProps<TData>) { }: DataGridTextCellProps<TData>) {
const isMultiline = const isMultiline =
@@ -74,8 +72,6 @@ export default function DataGridTextCell<TData extends object>({
if (event.key === 'Enter') { if (event.key === 'Enter') {
await handleSave(); await handleSave();
await focusCell();
cancelEditCell();
} }
} }
@@ -122,35 +118,12 @@ export default function DataGridTextCell<TData extends object>({
if (isEditing && isMultiline) { if (isEditing && isMultiline) {
return ( return (
<Input <Textarea
multiline ref={inputRef as Ref<HTMLTextAreaElement>}
ref={inputRef as Ref<HTMLInputElement>}
value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)} value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)}
onChange={handleChange} onChange={handleChange}
onKeyDown={handleTextAreaKeyDown} onKeyDown={handleTextAreaKeyDown}
fullWidth className="absolute left-0 top-0 z-10 h-25 min-h-25 w-full resize-none rounded-none !text-xs outline-none focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200"
className="absolute top-0 z-10 -mx-0.5 h-full min-h-38"
rows={5}
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/> />
); );
} }
@@ -162,84 +135,25 @@ export default function DataGridTextCell<TData extends object>({
value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)} value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)}
onChange={handleChange} onChange={handleChange}
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
fullWidth wrapperClassName="absolute top-0 z-10 w-full top-0 left-0 h-full"
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch" className="h-full w-full resize-none rounded-none border-none px-2 py-1.5 !text-xs outline-none focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/> />
); );
} }
if (!optimisticValue) { if (!optimisticValue) {
return ( return (
<Text className="truncate !text-xs" color="secondary"> <p className="truncate !text-xs text-[#7d8ca3]">
{optimisticValue === '' ? 'empty' : 'null'} {optimisticValue === '' ? 'empty' : 'null'}
</Text> </p>
);
}
if (isCopiable) {
return (
<div className="grid grid-flow-col items-center justify-start gap-1">
<Button
variant="borderless"
color="secondary"
onClick={(event) => {
event.stopPropagation();
const copiableValue =
typeof optimisticValue === 'object'
? JSON.stringify(optimisticValue)
: String(optimisticValue).replace(/\\n/gi, '\n');
copy(copiableValue, 'Value');
}}
className="-ml-px min-w-0 p-0"
aria-label="Copy value"
sx={{
color: (theme) =>
theme.palette.mode === 'dark'
? 'text.secondary'
: 'text.disabled',
}}
>
<CopyIcon className="h-4 w-4" />
</Button>
<Text className="truncate text-xs">
{typeof normalizedOptimisticValue === 'object'
? JSON.stringify(normalizedOptimisticValue)
: normalizedOptimisticValue}
</Text>
</div>
); );
} }
return ( return (
<Text className="truncate text-xs"> <p className="truncate text-xs">
{typeof normalizedOptimisticValue === 'object' {typeof normalizedOptimisticValue === 'object'
? JSON.stringify(normalizedOptimisticValue) ? JSON.stringify(normalizedOptimisticValue)
: normalizedOptimisticValue} : normalizedOptimisticValue}
</Text> </p>
); );
} }

View File

@@ -1,11 +1,11 @@
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 { DataGridDateCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDateCell'; import { DataGridDateCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDateCell';
import type { PreviewProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPreviewCell'; import type { PreviewProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPreviewCell';
import { DataGridPreviewCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPreviewCell'; import { DataGridPreviewCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPreviewCell';
import { DataGridTextCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridTextCell'; import { DataGridTextCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridTextCell';
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
import { FilePreviewIcon } from '@/components/ui/v2/icons/FilePreviewIcon'; import { FilePreviewIcon } from '@/components/ui/v2/icons/FilePreviewIcon';
import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient'; import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
import { useProject } from '@/features/orgs/projects/hooks/useProject'; import { useProject } from '@/features/orgs/projects/hooks/useProject';
@@ -93,7 +93,8 @@ const columns: (Column<StoredFile> & {
Header: 'isUploaded', Header: 'isUploaded',
accessor: 'isUploaded', accessor: 'isUploaded',
width: 100, width: 100,
Cell: DataGridBooleanCell, // eslint-disable-next-line react/prop-types
Cell: ({ value }) => <ReadOnlyToggle checked={value} />,
sortType: 'basic', sortType: 'basic',
}, },
{ {
@@ -342,6 +343,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
refetchData={refetchFilesAndAggregate} refetchData={refetchFilesAndAggregate}
/> />
} }
isFileDataGrid
{...props} {...props}
/> />
); );

View File

@@ -1,10 +1,8 @@
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 { Input, type InputProps } from '@/components/ui/v3/input';
import { Chip } from '@/components/ui/v2/Chip'; import { DataGridCustomizerControls } from '@/features/orgs/projects/common/components/DataGridCustomizerControls';
import type { InputProps } from '@/components/ui/v2/Input';
import { Input } from '@/components/ui/v2/Input';
import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient'; import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
import { useProject } from '@/features/orgs/projects/hooks/useProject'; import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider'; import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
@@ -12,6 +10,7 @@ import type { DataGridPaginationProps } from '@/features/orgs/projects/storage/d
import { DataGridPagination } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination'; import { DataGridPagination } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
import type { FileUploadButtonProps } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton'; import type { FileUploadButtonProps } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton';
import { FileUploadButton } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton'; import { FileUploadButton } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton';
import { cn } from '@/lib/utils';
import type { Files } from '@/utils/__generated__/graphql'; import type { Files } from '@/utils/__generated__/graphql';
import { getHasuraAdminSecret } from '@/utils/env'; import { getHasuraAdminSecret } from '@/utils/env';
import { triggerToast } from '@/utils/toast'; import { triggerToast } from '@/utils/toast';
@@ -22,11 +21,12 @@ import { twMerge } from 'tailwind-merge';
export type FilterProps = PropsWithoutRef<InputProps>; export type FilterProps = PropsWithoutRef<InputProps>;
export interface FilesDataGridControlsProps extends BoxProps { export interface FilesDataGridControlsProps {
paginationProps?: DataGridPaginationProps; paginationProps?: DataGridPaginationProps;
fileUploadProps?: FileUploadButtonProps; fileUploadProps?: FileUploadButtonProps;
filterProps?: FilterProps; filterProps?: FilterProps;
refetchData?: () => Promise<any>; refetchData?: () => Promise<any>;
className?: string;
} }
export default function FilesDataGridControls({ export default function FilesDataGridControls({
@@ -101,22 +101,23 @@ export default function FilesDataGridControls({
} }
return ( return (
<Box <div
className={twMerge('sticky top-0 z-20 border-b-1 p-2', className)} className={cn('box sticky top-0 z-20 border-b-1 p-2', className)}
{...props} {...props}
> >
{numberOfSelectedFiles > 0 ? ( {numberOfSelectedFiles > 0 ? (
<div className="mx-auto grid h-[40px] grid-flow-col items-center justify-start gap-2"> <div className="flex h-[40px] items-center justify-start gap-2">
<Chip <Badge
color="info" variant="secondary"
size="small" className="!bg-[#ebf3ff] text-primary dark:!bg-[#1b2534]"
label={`${numberOfSelectedFiles} selected`} >
/> {`${numberOfSelectedFiles} 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={deleteLoading} loading={deleteLoading}
onClick={() => onClick={() =>
openAlertDialog({ openAlertDialog({
@@ -145,22 +146,18 @@ export default function FilesDataGridControls({
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="mx-auto grid w-full grid-cols-12 gap-2"> <div className="flex w-full flex-grow gap-3">
<Input <Input
className={twMerge( wrapperClassName={cn('w-full', filterClassName)}
'col-span-12 xs+:col-span-12 md:col-span-9 xl:col-span-10',
filterClassName,
)}
fullWidth
{...restFilterProps} {...restFilterProps}
/> />
<div className="col-span-12 grid grid-flow-col gap-2 md:col-span-3 xl:col-span-2"> <div className="flex flex-shrink-0 gap-3">
<DataGridPagination <DataGridPagination
className={twMerge('col-span-6', paginationClassName)} className={twMerge('col-span-6', paginationClassName)}
{...restPaginationProps} {...restPaginationProps}
/> />
<DataGridCustomizerControls />
<FileUploadButton <FileUploadButton
className={twMerge( className={twMerge(
'col-span-6 self-stretch font-medium', 'col-span-6 self-stretch font-medium',
@@ -173,6 +170,6 @@ export default function FilesDataGridControls({
</div> </div>
</div> </div>
)} )}
</Box> </div>
); );
} }

View File

@@ -0,0 +1,51 @@
import type { RowDensity } from '@/features/orgs/projects/common/types/dataTableConfigurationTypes';
import PersistenDataTableConfigurationStorage from '@/features/orgs/projects/storage/dataGrid/utils/PersistenDataTableConfigurationStorage';
import {
createContext,
useContext,
useMemo,
useState,
type PropsWithChildren,
} from 'react';
type DataTableDesign = {
setRowDensity: (newDensity: RowDensity) => void;
rowDensity: RowDensity;
};
const DataTableDesignContext = createContext<DataTableDesign>({
rowDensity: 'comfortable',
setRowDensity: () => {},
});
function DataTableDesignProvider({ children }: PropsWithChildren) {
const [rowDensity, setRowDensity] = useState<RowDensity>(
() =>
PersistenDataTableConfigurationStorage.getDataTableViewConfiguration()
?.rowDensity ?? 'comfortable',
);
const contextValue: DataTableDesign = useMemo(
() => ({
rowDensity,
setRowDensity: (newRowDensity: RowDensity) => {
PersistenDataTableConfigurationStorage.saveRowDensity(newRowDensity);
setRowDensity(newRowDensity);
},
}),
[rowDensity],
);
return (
<DataTableDesignContext.Provider value={contextValue}>
{children}
</DataTableDesignContext.Provider>
);
}
export default DataTableDesignProvider;
export function useDataTableDesignContext() {
const context = useContext(DataTableDesignContext);
return context;
}

View File

@@ -0,0 +1,2 @@
export * from './DataTableDesignProvider';
export { default as DataTableDesignProvider } from './DataTableDesignProvider';

View File

@@ -0,0 +1,102 @@
import { localStorageMock, setInitialStore } from '@/tests/testUtils';
import PersistenDataTableConfigurationStorage, {
COLUMN_CONFIGURATION_STORAGE_KEY,
} from './PersistenDataTableConfigurationStorage';
describe('PersistenDataTableConfigurationStorage', () => {
const TABLE_PATH = 'default.public.myTable';
beforeAll(() => {
global.localStorage = localStorageMock();
});
beforeEach(() => {
setInitialStore({
[COLUMN_CONFIGURATION_STORAGE_KEY]: JSON.stringify({
[TABLE_PATH]: {
hiddenColumns: ['column1', 'column2'],
columnOrder: ['column3', 'column1', 'column2'],
},
}),
});
});
it('should return the hidden columns for the tablePath', () => {
const hiddenColumns =
PersistenDataTableConfigurationStorage.getHiddenColumns(TABLE_PATH);
expect(hiddenColumns).toStrictEqual(['column1', 'column2']);
});
it('should return an empty array if there are no hidden columns for the tablePath', () => {
const hiddenColumns =
PersistenDataTableConfigurationStorage.getHiddenColumns(
'default.public.no_hidden_columns',
);
expect(hiddenColumns).toStrictEqual([]);
});
it('should save the new hidden column state', () => {
PersistenDataTableConfigurationStorage.saveHiddenColumns(TABLE_PATH, []);
const hiddenColumns =
PersistenDataTableConfigurationStorage.getHiddenColumns(TABLE_PATH);
expect(hiddenColumns).toStrictEqual([]);
PersistenDataTableConfigurationStorage.saveHiddenColumns('newTable', [
'Hello',
'There',
]);
const newTableHiddenColumns =
PersistenDataTableConfigurationStorage.getHiddenColumns('newTable');
expect(newTableHiddenColumns).toStrictEqual(['Hello', 'There']);
});
it('should toggle the columns visibility', () => {
const hiddenColumns =
PersistenDataTableConfigurationStorage.getHiddenColumns(TABLE_PATH);
expect(hiddenColumns).toStrictEqual(['column1', 'column2']);
PersistenDataTableConfigurationStorage.toggleColumnVisibility(
TABLE_PATH,
'column2',
);
const updatedHiddenColumns =
PersistenDataTableConfigurationStorage.getHiddenColumns(TABLE_PATH);
expect(updatedHiddenColumns).toStrictEqual(['column1']);
});
it('should get the column order', () => {
const columnOrder =
PersistenDataTableConfigurationStorage.getColumnOrder(TABLE_PATH);
expect(columnOrder).toStrictEqual(['column3', 'column1', 'column2']);
});
it('should return an empty array when there is no saved state', () => {
const columnOrder = PersistenDataTableConfigurationStorage.getColumnOrder(
'default.public.no_saved_column_order',
);
expect(columnOrder).toStrictEqual([]);
});
it('should save the new column order', () => {
const columnOrder =
PersistenDataTableConfigurationStorage.getColumnOrder(TABLE_PATH);
expect(columnOrder).toStrictEqual(['column3', 'column1', 'column2']);
PersistenDataTableConfigurationStorage.saveColumnOrder(TABLE_PATH, [
'column2',
'column1',
]);
const newColumnOrder =
PersistenDataTableConfigurationStorage.getColumnOrder(TABLE_PATH);
expect(newColumnOrder).toStrictEqual(['column2', 'column1']);
});
});

Some files were not shown because too many files have changed in this diff Show More