From 2238cf37baac42d2cb114074c9d632eb94ff4424 Mon Sep 17 00:00:00 2001 From: RinYato Date: Mon, 15 Apr 2024 23:48:49 +0700 Subject: [PATCH] refactor: migrate to monorepo (#83) * refactor: migrate to monorepo * massive movement * fix(gui): error bundle and styling - bundle for browser to access webcrypto - add config to bundle css module - use the bundled css in client page * fix(gui): disable css-module in favor of tailwind * remove all css module * feat: transpile package for dev * refactor: move more code to gui * feat: fixing all eslint * fixing all the typecheck error * fixing typecheck * refactor: migrate to pnpm workspace * refactor(studio): change absolute path to `@studio/...` * refactor(gui): change absolute path to `@gui/...` * refactor: fix types error * refactor: formating * ci: update ci and add cache * fix: ci and test * fixing the typecheck * remove unused workspace * remove some part * fix: error on local client * fixing windows * using different check --------- Co-authored-by: Visal .In --- .github/workflows/check.yaml | 56 + .github/workflows/check.yml | 22 - .gitignore | 9 + .prettierignore | 11 + .vscode/settings.json | 3 +- config/eslint/base.js | 63 + config/eslint/next.js | 6 + config/eslint/package.json | 19 + config/eslint/react.js | 25 + config/eslint/tsconfig.json | 4 + config/tailwind/index.ts | 93 + config/tailwind/package.json | 17 + .../globals.css => config/tailwind/style.css | 0 config/tailwind/tsconfig.json | 4 + config/tsconfig/base.json | 35 + config/tsconfig/next.json | 14 + config/tsconfig/package.json | 11 + config/tsconfig/react.json | 10 + gui/.eslintrc.cjs | 29 + gui/.gitignore | 24 + gui/jest.config.cjs | 9 + gui/package.json | 98 + gui/postcss.config.js | 6 + .../src}/components/block-editor/editor.tsx | 3 +- .../block-editor/extensions/code-block.tsx | 13 +- .../block-editor/extensions/file-upload.ts | 2 +- .../block-editor/extensions/index.ts | 4 +- .../src}/components/block-editor/index.tsx | 1 + .../src}/components/block-editor/sheet.tsx | 2 +- .../src}/components/block-editor/style.css | 0 .../block-editor/suggestions/index.tsx | 10 +- .../suggestions/insert-code-block.tsx | 0 .../block-editor/utils/create-block-spec.tsx | 32 +- .../block-editor/utils/react-render.tsx | 9 +- .../src}/components/code-preview/index.tsx | 14 +- .../src}/components/connection-dialog.tsx | 14 +- .../src}/components/context-menu-handler.tsx | 10 +- .../src}/components/custom/ErrorMessage.tsx | 14 +- {src => gui/src}/components/database-gui.tsx | 170 +- .../src}/components/list-button-item.tsx | 2 +- .../src}/components/loading-opacity.tsx | 0 {src => gui/src}/components/logo-loading.tsx | 42 +- .../src}/components/main-connection.tsx | 135 +- .../src}/components/query-progress-log.tsx | 170 +- .../src}/components/query-result-table.tsx | 979 +- .../schema-editor/column-check-popup.tsx | 124 +- .../schema-editor/column-conflict-clause.tsx | 66 +- .../column-default-value-input.tsx | 372 +- .../schema-editor/column-fk-popup.tsx | 182 +- .../schema-editor/column-generate-popup.tsx | 178 +- .../schema-editor/column-pk-popup.tsx | 173 +- .../schema-editor/column-unique-popup.tsx | 116 +- .../src}/components/schema-editor/index.tsx | 300 +- .../schema-editor-column-list.tsx | 644 +- .../schema-editor-constraint-list.tsx | 356 +- .../src}/components/schema-sidebar-list.tsx | 458 +- .../src}/components/schema-sidebar.tsx | 155 +- {src => gui/src}/components/sidebar-tab.tsx | 244 +- .../components/sidebar/setting-sidebar.tsx | 2 +- {src => gui/src}/components/sortable-tab.tsx | 9 +- .../src}/components/sql-editor/index.tsx | 176 +- .../sql-editor/use-editor-theme.tsx | 142 +- .../components/table-cell/BigNumberCell.tsx | 46 +- .../components/table-cell/GenericCell.tsx | 217 +- .../src}/components/table-cell/NumberCell.tsx | 46 +- .../src}/components/table-cell/TextCell.tsx | 16 +- .../table-cell/createEditableCell.tsx | 441 +- .../table-cell/display-link-cell.tsx | 10 +- .../components/table-cell/styles.module.css | 34 +- .../table-combobox/TableColumnCombobox.tsx | 172 +- .../table-combobox/TableCombobox.tsx | 4 +- .../table-optimized/OptimizeTableState.tsx | 899 +- .../table-optimized/TableFakeBodyPadding.tsx | 2 +- .../table-optimized/TableFakeRowPadding.tsx | 0 .../table-optimized/TableHeader.tsx | 9 +- .../table-optimized/TableHeaderList.tsx | 0 .../TableHeaderResizeHandler.tsx | 3 +- .../src}/components/table-optimized/helper.ts | 2 +- .../src}/components/table-optimized/index.tsx | 52 +- .../useTableVisibilityRecalculation.ts | 4 +- .../src}/components/tabs/query-tab.tsx | 258 +- .../components/tabs/schema-editor-tab.tsx | 378 +- .../src}/components/tabs/table-data-tab.tsx | 659 +- .../src}/components/tabs/trigger-tab.tsx | 149 +- .../src}/components/tabs/users-tabs.tsx | 312 +- .../src}/components/ui/alert-dialog.tsx | 50 +- {src => gui/src}/components/ui/avatar.tsx | 22 +- {src => gui/src}/components/ui/button.tsx | 22 +- {src => gui/src}/components/ui/card.tsx | 37 +- {src => gui/src}/components/ui/checkbox.tsx | 16 +- gui/src/components/ui/command.tsx | 156 + .../src}/components/ui/context-menu.tsx | 68 +- {src => gui/src}/components/ui/dialog.tsx | 44 +- .../src}/components/ui/dropdown-menu.tsx | 68 +- {src => gui/src}/components/ui/hover-card.tsx | 18 +- {src => gui/src}/components/ui/input.tsx | 12 +- {src => gui/src}/components/ui/label.tsx | 18 +- {src => gui/src}/components/ui/menubar.tsx | 74 +- .../src}/components/ui/navigation-menu.tsx | 42 +- {src => gui/src}/components/ui/popover.tsx | 20 +- .../src}/components/ui/radio-group.tsx | 24 +- {src => gui/src}/components/ui/resizable.tsx | 18 +- .../src}/components/ui/scroll-area.tsx | 18 +- {src => gui/src}/components/ui/select.tsx | 46 +- {src => gui/src}/components/ui/separator.tsx | 14 +- {src => gui/src}/components/ui/sheet.tsx | 10 +- {src => gui/src}/components/ui/sonner.tsx | 2 +- {src => gui/src}/components/ui/textarea.tsx | 12 +- .../src}/components/ui/toggle-group.tsx | 28 +- {src => gui/src}/components/ui/toggle.tsx | 18 +- {src => gui/src}/components/ui/tooltip.tsx | 20 +- {src => gui/src}/components/windows-tab.tsx | 352 +- .../src/contexts/auto-complete-provider.tsx | 153 +- .../src/contexts}/block-editor-provider.tsx | 18 +- gui/src/contexts/config-provider.tsx | 33 + gui/src/contexts/driver-provider.tsx | 31 + .../src/contexts/schema-provider.tsx | 162 +- .../src/contexts}/theme-provider.tsx | 109 +- {src => gui/src}/drivers/base-driver.ts | 377 +- gui/src/drivers/collaboration-driver.ts | 8 + .../src}/drivers/sqlite-base-driver.ts | 63 +- {src => gui/src}/hooks/useElementResize.ts | 50 +- {src => gui/src}/hooks/useMessageListener.ts | 34 +- .../styles.module.css => gui/src/index.css | 81 +- gui/src/index.tsx | 10 + .../src/lib}/api-database-response.ts | 50 +- gui/src/lib/bit-operation.ts | 15 + {src => gui/src}/lib/export-helper.ts | 92 +- {src => gui/src}/lib/file-upload.ts | 8 +- gui/src/lib/internal-pubsub.ts | 30 + {src => gui/src}/lib/json-safe.ts | 25 +- {src => gui/src}/lib/key-matcher.ts | 0 {src => gui/src}/lib/multiple-query.ts | 154 +- {src => gui/src}/lib/sql-execute-helper.ts | 209 +- {src => gui/src}/lib/sql-generate.schema.ts | 320 +- gui/src/lib/utils.ts | 89 + {src => gui/src}/lib/validation.test.ts | 424 +- {src => gui/src}/lib/validation.ts | 183 +- {src => gui/src}/messages/const.ts | 8 +- .../src/messages/open-context-menu.tsx | 12 +- {src => gui/src}/messages/open-tab.tsx | 243 +- .../lib => gui/src/sqlite}/sql-helper.test.ts | 317 +- {src/lib => gui/src/sqlite}/sql-helper.ts | 277 +- .../src/sqlite}/sql-parse-table.test.ts | 414 +- .../lib => gui/src/sqlite}/sql-parse-table.ts | 970 +- .../src/sqlite}/sql-parse-trigger.test.ts | 0 .../src/sqlite}/sql-parse-trigger.ts | 2 +- gui/src/studio.tsx | 36 + gui/src/vite-env.d.ts | 1 + gui/tailwind.config.js | 10 + gui/tsconfig.json | 20 + gui/tsup.config.ts | 15 + gui/vite.config.ts | 11 + {src => gui}/window.d.ts | 2 +- next.config.js | 6 - package-lock.json | 16762 ---------------- package.json | 115 +- pnpm-lock.yaml | 13245 ++++++++++++ pnpm-workspace.yaml | 4 + .../api/database/[database_id]/ops/route.ts | 27 - src/app/api/ops/[database_id]/route.ts | 36 - src/app/client/[[...driver]]/page-client.tsx | 49 - src/app/client/r/page-client.tsx | 42 - .../client/s/[[...driver]]/page-client.tsx | 49 - src/app/storybook/connection_error/page.tsx | 11 - src/app/storybook/opacity_loading/page.tsx | 9 - src/app/storybook/query_progress/page.tsx | 65 - src/components/sortable-tab.module.css | 7 - src/context/DatabaseDriverProvider.tsx | 31 - .eslintrc.json => studio/.eslintrc.json | 3 +- studio/.gitignore | 37 + studio/.prettierignore | 1 + studio/.prettierrc | 6 + studio/LICENSE | 21 + studio/README.md | 14 + components.json => studio/components.json | 2 +- drizzle.config.ts => studio/drizzle.config.ts | 24 +- .../drizzle}/0000_solid_scream.sql | 0 .../drizzle}/0001_crazy_eternals.sql | 0 .../drizzle}/0002_aspiring_shotgun.sql | 0 .../drizzle}/0003_dashing_marauders.sql | 0 .../drizzle}/0004_misty_the_professor.sql | 0 .../drizzle}/0005_fine_turbo.sql | 0 .../drizzle}/0006_whole_tomorrow_man.sql | 0 .../drizzle}/meta/0000_snapshot.json | 0 .../drizzle}/meta/0001_snapshot.json | 0 .../drizzle}/meta/0002_snapshot.json | 0 .../drizzle}/meta/0003_snapshot.json | 0 .../drizzle}/meta/0004_snapshot.json | 0 .../drizzle}/meta/0005_snapshot.json | 0 .../drizzle}/meta/0006_snapshot.json | 0 .../drizzle}/meta/_journal.json | 0 jest.config.ts => studio/jest.config.ts | 36 +- studio/next.config.js | 14 + studio/package.json | 111 + .../postcss.config.cjs | 2 +- {public => studio/public}/data-editor.png | Bin {public => studio/public}/edit-table.png | Bin {public => studio/public}/libsql-logo.png | Bin {public => studio/public}/logo.svg | 0 {public => studio/public}/next.svg | 0 {public => studio/public}/open-source.png | Bin {public => studio/public}/rqlite.png | Bin {public => studio/public}/screenshot.png | Bin {public => studio/public}/screenshot2.png | Bin {public => studio/public}/sql-editor.png | Bin {public => studio/public}/turso.jpeg | Bin {public => studio/public}/turso.png | Bin {public => studio/public}/valtown.svg | 0 {public => studio/public}/vercel.svg | 0 script.sh => studio/script.sh | 0 .../sonar-project.properties | 0 {src => studio/src}/app/about/page.tsx | 6 +- .../[database_id]/ops/handle-assign-user.ts | 224 +- .../[database_id]/ops/handle-delete-user.ts | 156 +- .../[database_id]/ops/handle-role-list.ts | 40 +- .../[database_id]/ops/handle-users-list.ts | 80 +- .../api/database/[database_id]/ops/route.ts | 27 + .../app/api/database/[database_id]/route.ts | 204 +- {src => studio/src}/app/api/database/route.ts | 232 +- .../src}/app/api/databases/route.ts | 90 +- .../app/api/ops/[database_id]/handle-batch.ts | 58 +- .../app/api/ops/[database_id]/handle-query.ts | 58 +- .../api/ops/[database_id]/handle-schema.ts | 40 +- .../api/ops/[database_id]/handle-schemas.ts | 60 +- .../ops/[database_id]/handle-select-table.ts | 84 +- .../api/ops/[database_id]/handle-trigger.ts | 40 +- .../[database_id]/handle-update-table-data.ts | 46 +- studio/src/app/api/ops/[database_id]/route.ts | 36 + .../ops/[database_id]/turso-edge-client.ts | 54 +- {src => studio/src}/app/api/upload/route.ts | 18 +- {src => studio/src}/app/blog/page.tsx | 6 +- .../app/client/[[...driver]]/page-client.tsx | 35 + .../src/app/client}/[[...driver]]/page.tsx | 18 +- studio/src/app/client/r/page-client.tsx | 50 + {src => studio/src}/app/client/r/page.tsx | 90 +- .../client/s/[[...driver]]/page-client.tsx | 59 + .../src/app/client/s}/[[...driver]]/page.tsx | 18 +- .../src}/app/connect/connection-list.tsx | 458 +- .../app/connect/connection-string-input.tsx | 170 +- .../src}/app/connect/driver-dropdown.tsx | 132 +- .../src}/app/connect/page-client.tsx | 6 +- {src => studio/src}/app/connect/page.tsx | 2 +- .../src}/app/connect/quick-connect.tsx | 124 +- .../app/connect/saved-connection-card.tsx | 182 +- .../app/connect/saved-connection-config.tsx | 272 +- .../app/connect/saved-connection-content.tsx | 74 +- .../app/connect/saved-connection-storage.ts | 654 +- .../app/connect/saved-connection-type.tsx | 124 +- .../src}/app/connect/saved-connection.tsx | 218 +- .../app/connect/saved-edit-connection.tsx | 340 +- .../app/connect/saved-remove-connection.tsx | 168 +- {src => studio/src}/app/favicon.ico | Bin studio/src/app/globals.css | 99 + {src => studio/src}/app/layout.tsx | 4 +- .../src}/app/login/github/callback/route.ts | 160 +- {src => studio/src}/app/login/github/route.ts | 40 +- .../src}/app/login/google/callback/route.ts | 4 +- {src => studio/src}/app/login/google/route.ts | 2 +- {src => studio/src}/app/login/page.tsx | 70 +- {src => studio/src}/app/logout/route.tsx | 52 +- {src => studio/src}/app/page.tsx | 21 +- {src => studio/src}/cli/generate-aes-key.ts | 6 +- {src => studio/src}/components/logout.tsx | 46 +- .../src}/components/top-navigation.tsx | 120 +- .../src}/components/topbar-profile.tsx | 86 +- studio/src/components/ui/alert-dialog.tsx | 141 + studio/src/components/ui/avatar.tsx | 50 + studio/src/components/ui/button.tsx | 57 + studio/src/components/ui/card.tsx | 83 + studio/src/components/ui/checkbox.tsx | 30 + {src => studio/src}/components/ui/command.tsx | 54 +- studio/src/components/ui/context-menu.tsx | 204 + studio/src/components/ui/dialog.tsx | 122 + studio/src/components/ui/dropdown-menu.tsx | 205 + studio/src/components/ui/hover-card.tsx | 29 + studio/src/components/ui/input.tsx | 25 + studio/src/components/ui/label.tsx | 26 + studio/src/components/ui/menubar.tsx | 240 + studio/src/components/ui/navigation-menu.tsx | 128 + studio/src/components/ui/popover.tsx | 33 + studio/src/components/ui/radio-group.tsx | 44 + studio/src/components/ui/resizable.tsx | 45 + studio/src/components/ui/scroll-area.tsx | 48 + studio/src/components/ui/select.tsx | 164 + studio/src/components/ui/separator.tsx | 31 + studio/src/components/ui/sheet.tsx | 144 + studio/src/components/ui/sonner.tsx | 33 + studio/src/components/ui/textarea.tsx | 24 + studio/src/components/ui/toggle-group.tsx | 61 + studio/src/components/ui/toggle.tsx | 45 + studio/src/components/ui/tooltip.tsx | 30 + {src => studio/src}/constants/http-status.ts | 0 .../context/connection-config-provider.tsx | 2 +- studio/src/context/theme-provider.tsx | 54 + {src => studio/src}/controllers/auth.ts | 6 +- {src => studio/src}/db/index.ts | 22 +- {src => studio/src}/db/migrate.ts | 32 +- {src => studio/src}/db/schema-database.ts | 242 +- {src => studio/src}/db/schema-user.ts | 142 +- {src => studio/src}/db/schema.ts | 4 +- .../src}/drivers/collaboration-driver.ts | 99 +- {src => studio/src}/drivers/remote-driver.ts | 214 +- {src => studio/src}/drivers/rqlite-driver.ts | 10 +- {src => studio/src}/drivers/turso-driver.tsx | 170 +- {src => studio/src}/drivers/valtown-driver.ts | 3 +- {src => studio/src}/env.ts | 92 +- {src => studio/src}/hooks/use-connect.ts | 58 +- {src => studio/src}/lib/api-error.ts | 0 {src => studio/src}/lib/api-response-types.ts | 81 +- .../src}/lib/api/api-database-request.ts | 48 +- studio/src/lib/api/api-database-response.ts | 25 + .../src}/lib/api/api-request-types.ts | 112 +- .../src}/lib/api/fetch-databases.ts | 108 +- {src => studio/src}/lib/auth.ts | 256 +- {src => studio/src}/lib/bit-operation.ts | 30 +- {src => studio/src}/lib/encryption-edge.ts | 130 +- {src => studio/src}/lib/encryption.test.ts | 16 +- {src => studio/src}/lib/encryption.ts | 60 +- studio/src/lib/file-upload.ts | 107 + {src => studio/src}/lib/hash-edge.ts | 0 {src => studio/src}/lib/internal-pubsub.ts | 0 studio/src/lib/json-safe.ts | 13 + studio/src/lib/key-matcher.ts | 79 + {src => studio/src}/lib/r2.ts | 4 +- {src => studio/src}/lib/type-utils.ts | 0 {src => studio/src}/lib/utils.ts | 4 +- studio/src/lib/validation.ts | 8 + {src => studio/src}/lib/with-database-ops.ts | 214 +- {src => studio/src}/lib/with-user.ts | 102 +- .../tailwind.config.ts | 0 studio/tsconfig.json | 28 + tsconfig.json | 27 +- turbo.json | 31 + 334 files changed, 28416 insertions(+), 28404 deletions(-) create mode 100644 .github/workflows/check.yaml delete mode 100644 .github/workflows/check.yml create mode 100644 .prettierignore create mode 100644 config/eslint/base.js create mode 100644 config/eslint/next.js create mode 100644 config/eslint/package.json create mode 100644 config/eslint/react.js create mode 100644 config/eslint/tsconfig.json create mode 100644 config/tailwind/index.ts create mode 100644 config/tailwind/package.json rename src/app/globals.css => config/tailwind/style.css (100%) create mode 100644 config/tailwind/tsconfig.json create mode 100644 config/tsconfig/base.json create mode 100644 config/tsconfig/next.json create mode 100644 config/tsconfig/package.json create mode 100644 config/tsconfig/react.json create mode 100644 gui/.eslintrc.cjs create mode 100644 gui/.gitignore create mode 100644 gui/jest.config.cjs create mode 100644 gui/package.json create mode 100644 gui/postcss.config.js rename {src => gui/src}/components/block-editor/editor.tsx (83%) rename {src => gui/src}/components/block-editor/extensions/code-block.tsx (94%) rename {src => gui/src}/components/block-editor/extensions/file-upload.ts (80%) rename {src => gui/src}/components/block-editor/extensions/index.ts (90%) rename {src => gui/src}/components/block-editor/index.tsx (85%) rename {src => gui/src}/components/block-editor/sheet.tsx (96%) rename {src => gui/src}/components/block-editor/style.css (100%) rename {src => gui/src}/components/block-editor/suggestions/index.tsx (83%) rename {src => gui/src}/components/block-editor/suggestions/insert-code-block.tsx (100%) rename {src => gui/src}/components/block-editor/utils/create-block-spec.tsx (92%) rename {src => gui/src}/components/block-editor/utils/react-render.tsx (85%) rename {src => gui/src}/components/code-preview/index.tsx (96%) rename {src => gui/src}/components/connection-dialog.tsx (66%) rename {src => gui/src}/components/context-menu-handler.tsx (93%) rename {src => gui/src}/components/custom/ErrorMessage.tsx (95%) rename {src => gui/src}/components/database-gui.tsx (83%) rename {src => gui/src}/components/list-button-item.tsx (94%) rename {src => gui/src}/components/loading-opacity.tsx (100%) rename {src => gui/src}/components/logo-loading.tsx (96%) rename {src => gui/src}/components/main-connection.tsx (60%) rename {src => gui/src}/components/query-progress-log.tsx (93%) rename {src => gui/src}/components/query-result-table.tsx (91%) rename {src => gui/src}/components/schema-editor/column-check-popup.tsx (92%) rename {src => gui/src}/components/schema-editor/column-conflict-clause.tsx (89%) rename {src => gui/src}/components/schema-editor/column-default-value-input.tsx (91%) rename {src => gui/src}/components/schema-editor/column-fk-popup.tsx (94%) rename {src => gui/src}/components/schema-editor/column-generate-popup.tsx (93%) rename {src => gui/src}/components/schema-editor/column-pk-popup.tsx (93%) rename {src => gui/src}/components/schema-editor/column-unique-popup.tsx (92%) rename {src => gui/src}/components/schema-editor/index.tsx (92%) rename {src => gui/src}/components/schema-editor/schema-editor-column-list.tsx (91%) rename {src => gui/src}/components/schema-editor/schema-editor-constraint-list.tsx (93%) rename {src => gui/src}/components/schema-sidebar-list.tsx (91%) rename {src => gui/src}/components/schema-sidebar.tsx (85%) rename {src => gui/src}/components/sidebar-tab.tsx (80%) rename {src => gui/src}/components/sidebar/setting-sidebar.tsx (88%) rename {src => gui/src}/components/sortable-tab.tsx (90%) rename {src => gui/src}/components/sql-editor/index.tsx (93%) rename {src => gui/src}/components/sql-editor/use-editor-theme.tsx (95%) rename {src => gui/src}/components/table-cell/BigNumberCell.tsx (95%) rename {src => gui/src}/components/table-cell/GenericCell.tsx (86%) rename {src => gui/src}/components/table-cell/NumberCell.tsx (95%) rename {src => gui/src}/components/table-cell/TextCell.tsx (95%) rename {src => gui/src}/components/table-cell/createEditableCell.tsx (90%) rename {src => gui/src}/components/table-cell/display-link-cell.tsx (88%) rename {src => gui/src}/components/table-cell/styles.module.css (92%) rename {src => gui/src}/components/table-combobox/TableColumnCombobox.tsx (90%) rename {src => gui/src}/components/table-combobox/TableCombobox.tsx (96%) rename {src => gui/src}/components/table-optimized/OptimizeTableState.tsx (89%) rename {src => gui/src}/components/table-optimized/TableFakeBodyPadding.tsx (95%) rename {src => gui/src}/components/table-optimized/TableFakeRowPadding.tsx (100%) rename {src => gui/src}/components/table-optimized/TableHeader.tsx (79%) rename {src => gui/src}/components/table-optimized/TableHeaderList.tsx (100%) rename {src => gui/src}/components/table-optimized/TableHeaderResizeHandler.tsx (97%) rename {src => gui/src}/components/table-optimized/helper.ts (96%) rename {src => gui/src}/components/table-optimized/index.tsx (87%) rename {src => gui/src}/components/table-optimized/useTableVisibilityRecalculation.ts (93%) rename {src => gui/src}/components/tabs/query-tab.tsx (81%) rename {src => gui/src}/components/tabs/schema-editor-tab.tsx (85%) rename {src => gui/src}/components/tabs/table-data-tab.tsx (90%) rename {src => gui/src}/components/tabs/trigger-tab.tsx (86%) rename {src => gui/src}/components/tabs/users-tabs.tsx (90%) rename {src => gui/src}/components/ui/alert-dialog.tsx (87%) rename {src => gui/src}/components/ui/avatar.tsx (77%) rename {src => gui/src}/components/ui/button.tsx (85%) rename {src => gui/src}/components/ui/card.tsx (77%) rename {src => gui/src}/components/ui/checkbox.tsx (73%) create mode 100644 gui/src/components/ui/command.tsx rename {src => gui/src}/components/ui/context-menu.tsx (89%) rename {src => gui/src}/components/ui/dialog.tsx (82%) rename {src => gui/src}/components/ui/dropdown-menu.tsx (88%) rename {src => gui/src}/components/ui/hover-card.tsx (79%) rename {src => gui/src}/components/ui/input.tsx (84%) rename {src => gui/src}/components/ui/label.tsx (71%) rename {src => gui/src}/components/ui/menubar.tsx (90%) rename {src => gui/src}/components/ui/navigation-menu.tsx (90%) rename {src => gui/src}/components/ui/popover.tsx (78%) rename {src => gui/src}/components/ui/radio-group.tsx (82%) rename {src => gui/src}/components/ui/resizable.tsx (81%) rename {src => gui/src}/components/ui/scroll-area.tsx (87%) rename {src => gui/src}/components/ui/select.tsx (88%) rename {src => gui/src}/components/ui/separator.tsx (70%) rename {src => gui/src}/components/ui/sheet.tsx (97%) rename {src => gui/src}/components/ui/sonner.tsx (94%) rename {src => gui/src}/components/ui/textarea.tsx (81%) rename {src => gui/src}/components/ui/toggle-group.tsx (77%) rename {src => gui/src}/components/ui/toggle.tsx (82%) rename {src => gui/src}/components/ui/tooltip.tsx (74%) rename {src => gui/src}/components/windows-tab.tsx (86%) rename src/context/AutoCompleteProvider.tsx => gui/src/contexts/auto-complete-provider.tsx (77%) rename {src/context => gui/src/contexts}/block-editor-provider.tsx (86%) create mode 100644 gui/src/contexts/config-provider.tsx create mode 100644 gui/src/contexts/driver-provider.tsx rename src/context/SchemaProvider.tsx => gui/src/contexts/schema-provider.tsx (85%) rename {src/context => gui/src/contexts}/theme-provider.tsx (95%) rename {src => gui/src}/drivers/base-driver.ts (91%) create mode 100644 gui/src/drivers/collaboration-driver.ts rename {src => gui/src}/drivers/sqlite-base-driver.ts (81%) rename {src => gui/src}/hooks/useElementResize.ts (96%) rename {src => gui/src}/hooks/useMessageListener.ts (66%) rename src/components/table-optimized/styles.module.css => gui/src/index.css (53%) create mode 100644 gui/src/index.tsx rename {src/lib/api => gui/src/lib}/api-database-response.ts (93%) create mode 100644 gui/src/lib/bit-operation.ts rename {src => gui/src}/lib/export-helper.ts (83%) rename {src => gui/src}/lib/file-upload.ts (96%) create mode 100644 gui/src/lib/internal-pubsub.ts rename {src => gui/src}/lib/json-safe.ts (76%) rename {src => gui/src}/lib/key-matcher.ts (100%) rename {src => gui/src}/lib/multiple-query.ts (89%) rename {src => gui/src}/lib/sql-execute-helper.ts (81%) rename {src => gui/src}/lib/sql-generate.schema.ts (92%) create mode 100644 gui/src/lib/utils.ts rename {src => gui/src}/lib/validation.test.ts (87%) rename {src => gui/src}/lib/validation.ts (75%) rename {src => gui/src}/messages/const.ts (96%) rename src/messages/openContextMenu.tsx => gui/src/messages/open-context-menu.tsx (78%) rename {src => gui/src}/messages/open-tab.tsx (71%) rename {src/lib => gui/src/sqlite}/sql-helper.test.ts (81%) rename {src/lib => gui/src/sqlite}/sql-helper.ts (78%) rename {src/lib => gui/src/sqlite}/sql-parse-table.test.ts (95%) rename {src/lib => gui/src/sqlite}/sql-parse-table.ts (94%) rename {src/lib => gui/src/sqlite}/sql-parse-trigger.test.ts (100%) rename {src/lib => gui/src/sqlite}/sql-parse-trigger.ts (98%) create mode 100644 gui/src/studio.tsx create mode 100644 gui/src/vite-env.d.ts create mode 100644 gui/tailwind.config.js create mode 100644 gui/tsconfig.json create mode 100644 gui/tsup.config.ts create mode 100644 gui/vite.config.ts rename {src => gui}/window.d.ts (62%) delete mode 100644 next.config.js delete mode 100644 package-lock.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml delete mode 100644 src/app/api/database/[database_id]/ops/route.ts delete mode 100644 src/app/api/ops/[database_id]/route.ts delete mode 100644 src/app/client/[[...driver]]/page-client.tsx delete mode 100644 src/app/client/r/page-client.tsx delete mode 100644 src/app/client/s/[[...driver]]/page-client.tsx delete mode 100644 src/app/storybook/connection_error/page.tsx delete mode 100644 src/app/storybook/opacity_loading/page.tsx delete mode 100644 src/app/storybook/query_progress/page.tsx delete mode 100644 src/components/sortable-tab.module.css delete mode 100644 src/context/DatabaseDriverProvider.tsx rename .eslintrc.json => studio/.eslintrc.json (82%) create mode 100644 studio/.gitignore create mode 100644 studio/.prettierignore create mode 100644 studio/.prettierrc create mode 100644 studio/LICENSE create mode 100644 studio/README.md rename components.json => studio/components.json (99%) rename drizzle.config.ts => studio/drizzle.config.ts (85%) rename {drizzle => studio/drizzle}/0000_solid_scream.sql (100%) rename {drizzle => studio/drizzle}/0001_crazy_eternals.sql (100%) rename {drizzle => studio/drizzle}/0002_aspiring_shotgun.sql (100%) rename {drizzle => studio/drizzle}/0003_dashing_marauders.sql (100%) rename {drizzle => studio/drizzle}/0004_misty_the_professor.sql (100%) rename {drizzle => studio/drizzle}/0005_fine_turbo.sql (100%) rename {drizzle => studio/drizzle}/0006_whole_tomorrow_man.sql (100%) rename {drizzle => studio/drizzle}/meta/0000_snapshot.json (100%) rename {drizzle => studio/drizzle}/meta/0001_snapshot.json (100%) rename {drizzle => studio/drizzle}/meta/0002_snapshot.json (100%) rename {drizzle => studio/drizzle}/meta/0003_snapshot.json (100%) rename {drizzle => studio/drizzle}/meta/0004_snapshot.json (100%) rename {drizzle => studio/drizzle}/meta/0005_snapshot.json (100%) rename {drizzle => studio/drizzle}/meta/0006_snapshot.json (100%) rename {drizzle => studio/drizzle}/meta/_journal.json (100%) rename jest.config.ts => studio/jest.config.ts (97%) create mode 100644 studio/next.config.js create mode 100644 studio/package.json rename postcss.config.js => studio/postcss.config.cjs (96%) rename {public => studio/public}/data-editor.png (100%) rename {public => studio/public}/edit-table.png (100%) rename {public => studio/public}/libsql-logo.png (100%) rename {public => studio/public}/logo.svg (100%) rename {public => studio/public}/next.svg (100%) rename {public => studio/public}/open-source.png (100%) rename {public => studio/public}/rqlite.png (100%) rename {public => studio/public}/screenshot.png (100%) rename {public => studio/public}/screenshot2.png (100%) rename {public => studio/public}/sql-editor.png (100%) rename {public => studio/public}/turso.jpeg (100%) rename {public => studio/public}/turso.png (100%) rename {public => studio/public}/valtown.svg (100%) rename {public => studio/public}/vercel.svg (100%) rename script.sh => studio/script.sh (100%) rename sonar-project.properties => studio/sonar-project.properties (100%) rename {src => studio/src}/app/about/page.tsx (96%) rename {src => studio/src}/app/api/database/[database_id]/ops/handle-assign-user.ts (84%) rename {src => studio/src}/app/api/database/[database_id]/ops/handle-delete-user.ts (81%) rename {src => studio/src}/app/api/database/[database_id]/ops/handle-role-list.ts (71%) rename {src => studio/src}/app/api/database/[database_id]/ops/handle-users-list.ts (80%) create mode 100644 studio/src/app/api/database/[database_id]/ops/route.ts rename {src => studio/src}/app/api/database/[database_id]/route.ts (86%) rename {src => studio/src}/app/api/database/route.ts (87%) rename {src => studio/src}/app/api/databases/route.ts (78%) rename {src => studio/src}/app/api/ops/[database_id]/handle-batch.ts (80%) rename {src => studio/src}/app/api/ops/[database_id]/handle-query.ts (80%) rename {src => studio/src}/app/api/ops/[database_id]/handle-schema.ts (76%) rename {src => studio/src}/app/api/ops/[database_id]/handle-schemas.ts (89%) rename {src => studio/src}/app/api/ops/[database_id]/handle-select-table.ts (78%) rename {src => studio/src}/app/api/ops/[database_id]/handle-trigger.ts (75%) rename {src => studio/src}/app/api/ops/[database_id]/handle-update-table-data.ts (78%) create mode 100644 studio/src/app/api/ops/[database_id]/route.ts rename {src => studio/src}/app/api/ops/[database_id]/turso-edge-client.ts (66%) rename {src => studio/src}/app/api/upload/route.ts (87%) rename {src => studio/src}/app/blog/page.tsx (96%) create mode 100644 studio/src/app/client/[[...driver]]/page-client.tsx rename {src/app/client/s => studio/src/app/client}/[[...driver]]/page.tsx (95%) create mode 100644 studio/src/app/client/r/page-client.tsx rename {src => studio/src}/app/client/r/page.tsx (78%) create mode 100644 studio/src/app/client/s/[[...driver]]/page-client.tsx rename {src/app/client => studio/src/app/client/s}/[[...driver]]/page.tsx (95%) rename {src => studio/src}/app/connect/connection-list.tsx (94%) rename {src => studio/src}/app/connect/connection-string-input.tsx (93%) rename {src => studio/src}/app/connect/driver-dropdown.tsx (94%) rename {src => studio/src}/app/connect/page-client.tsx (94%) rename {src => studio/src}/app/connect/page.tsx (84%) rename {src => studio/src}/app/connect/quick-connect.tsx (91%) rename {src => studio/src}/app/connect/saved-connection-card.tsx (94%) rename {src => studio/src}/app/connect/saved-connection-config.tsx (88%) rename {src => studio/src}/app/connect/saved-connection-content.tsx (96%) rename {src => studio/src}/app/connect/saved-connection-storage.ts (94%) rename {src => studio/src}/app/connect/saved-connection-type.tsx (86%) rename {src => studio/src}/app/connect/saved-connection.tsx (93%) rename {src => studio/src}/app/connect/saved-edit-connection.tsx (94%) rename {src => studio/src}/app/connect/saved-remove-connection.tsx (88%) rename {src => studio/src}/app/favicon.ico (100%) create mode 100644 studio/src/app/globals.css rename {src => studio/src}/app/layout.tsx (89%) rename {src => studio/src}/app/login/github/callback/route.ts (91%) rename {src => studio/src}/app/login/github/route.ts (89%) rename {src => studio/src}/app/login/google/callback/route.ts (93%) rename {src => studio/src}/app/login/google/route.ts (94%) rename {src => studio/src}/app/login/page.tsx (84%) rename {src => studio/src}/app/logout/route.tsx (85%) rename {src => studio/src}/app/page.tsx (92%) rename {src => studio/src}/cli/generate-aes-key.ts (97%) rename {src => studio/src}/components/logout.tsx (85%) rename {src => studio/src}/components/top-navigation.tsx (92%) rename {src => studio/src}/components/topbar-profile.tsx (96%) create mode 100644 studio/src/components/ui/alert-dialog.tsx create mode 100644 studio/src/components/ui/avatar.tsx create mode 100644 studio/src/components/ui/button.tsx create mode 100644 studio/src/components/ui/card.tsx create mode 100644 studio/src/components/ui/checkbox.tsx rename {src => studio/src}/components/ui/command.tsx (83%) create mode 100644 studio/src/components/ui/context-menu.tsx create mode 100644 studio/src/components/ui/dialog.tsx create mode 100644 studio/src/components/ui/dropdown-menu.tsx create mode 100644 studio/src/components/ui/hover-card.tsx create mode 100644 studio/src/components/ui/input.tsx create mode 100644 studio/src/components/ui/label.tsx create mode 100644 studio/src/components/ui/menubar.tsx create mode 100644 studio/src/components/ui/navigation-menu.tsx create mode 100644 studio/src/components/ui/popover.tsx create mode 100644 studio/src/components/ui/radio-group.tsx create mode 100644 studio/src/components/ui/resizable.tsx create mode 100644 studio/src/components/ui/scroll-area.tsx create mode 100644 studio/src/components/ui/select.tsx create mode 100644 studio/src/components/ui/separator.tsx create mode 100644 studio/src/components/ui/sheet.tsx create mode 100644 studio/src/components/ui/sonner.tsx create mode 100644 studio/src/components/ui/textarea.tsx create mode 100644 studio/src/components/ui/toggle-group.tsx create mode 100644 studio/src/components/ui/toggle.tsx create mode 100644 studio/src/components/ui/tooltip.tsx rename {src => studio/src}/constants/http-status.ts (100%) rename {src => studio/src}/context/connection-config-provider.tsx (88%) create mode 100644 studio/src/context/theme-provider.tsx rename {src => studio/src}/controllers/auth.ts (91%) rename {src => studio/src}/db/index.ts (88%) rename {src => studio/src}/db/migrate.ts (96%) rename {src => studio/src}/db/schema-database.ts (96%) rename {src => studio/src}/db/schema-user.ts (95%) rename {src => studio/src}/db/schema.ts (97%) rename {src => studio/src}/drivers/collaboration-driver.ts (73%) rename {src => studio/src}/drivers/remote-driver.ts (91%) rename {src => studio/src}/drivers/rqlite-driver.ts (93%) rename {src => studio/src}/drivers/turso-driver.tsx (87%) rename {src => studio/src}/drivers/valtown-driver.ts (92%) rename {src => studio/src}/env.ts (96%) rename {src => studio/src}/hooks/use-connect.ts (89%) rename {src => studio/src}/lib/api-error.ts (100%) rename {src => studio/src}/lib/api-response-types.ts (82%) rename {src => studio/src}/lib/api/api-database-request.ts (95%) create mode 100644 studio/src/lib/api/api-database-response.ts rename {src => studio/src}/lib/api/api-request-types.ts (92%) rename {src => studio/src}/lib/api/fetch-databases.ts (92%) rename {src => studio/src}/lib/auth.ts (89%) rename {src => studio/src}/lib/bit-operation.ts (96%) rename {src => studio/src}/lib/encryption-edge.ts (96%) rename {src => studio/src}/lib/encryption.test.ts (97%) rename {src => studio/src}/lib/encryption.ts (96%) create mode 100644 studio/src/lib/file-upload.ts rename {src => studio/src}/lib/hash-edge.ts (100%) rename {src => studio/src}/lib/internal-pubsub.ts (100%) create mode 100644 studio/src/lib/json-safe.ts create mode 100644 studio/src/lib/key-matcher.ts rename {src => studio/src}/lib/r2.ts (94%) rename {src => studio/src}/lib/type-utils.ts (100%) rename {src => studio/src}/lib/utils.ts (97%) create mode 100644 studio/src/lib/validation.ts rename {src => studio/src}/lib/with-database-ops.ts (94%) rename {src => studio/src}/lib/with-user.ts (84%) rename tailwind.config.ts => studio/tailwind.config.ts (100%) create mode 100644 studio/tsconfig.json create mode 100644 turbo.json diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 00000000..88f2fdda --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +1,56 @@ +name: Check + +on: + push: + branches: + - develop + pull_request: + workflow_dispatch: + +jobs: + quality-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: 8.13.1 + run_install: false + + - uses: actions/setup-node@v3 + with: + node-version: 20 + cache: pnpm + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Cache pnpm + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Cache turbo tasks + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + + - name: Install dependencies + run: pnpm install + + - name: Run Check + run: pnpm run staged + + - name: Log success + run: echo "✅ Success!" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml deleted file mode 100644 index 0ec007cb..00000000 --- a/.github/workflows/check.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Check -on: - push: - branches: - - develop - pull_request: -jobs: - check: - name: Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Install dependencies - run: npm install - - name: Check type - run: npm run tsc - - name: Check Lint - run: npm run lint - - name: Check Test - run: npm run test diff --git a/.gitignore b/.gitignore index 00bba9bb..49deb113 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # dependencies /node_modules +**/node_modules /.pnp .pnp.js .yarn/install-state.gz @@ -10,13 +11,16 @@ /coverage # next.js +**/.next /.next/ /out/ # production /build +**/dist # misc +**/.DS_Store .DS_Store *.pem @@ -32,6 +36,11 @@ yarn-error.log* # vercel .vercel +# turbo +**/.turbo +.turbo + # typescript +**/*.tsbuildinfo *.tsbuildinfo next-env.d.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..34a0489e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +package-lock.json +.next +dist +drizzle/* +**/*/drizzle +.prettierignore +.gitignore + +.changeset/ + + diff --git a/.vscode/settings.json b/.vscode/settings.json index 0399a78b..5d3253ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "typescript.tsdk": "node_modules\\typescript\\lib" } \ No newline at end of file diff --git a/config/eslint/base.js b/config/eslint/base.js new file mode 100644 index 00000000..53c2525a --- /dev/null +++ b/config/eslint/base.js @@ -0,0 +1,63 @@ +/** @type {import('eslint').Linter.Config} */ +const config = { + ignorePatterns: ["node_modules", "dist", ".next"], + env: { + es2022: true, + node: true, + }, + extends: ["plugin:eslint-comments/recommended", "prettier"], + overrides: [ + { + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + "**/*.mjs", + "**/*.cjs", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, + plugins: ["@typescript-eslint"], + extends: [ + "plugin:@typescript-eslint/strict-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", + ], + rules: { + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/prefer-nullish-coalescing": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/array-type": [ + "error", + { + default: "array-simple", + readonly: "array-simple", + }, + ], + "@typescript-eslint/consistent-type-imports": [ + "warn", + { + prefer: "type-imports", + fixStyle: "inline-type-imports", + }, + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_" }, + ], + "@typescript-eslint/require-await": "error", + "@typescript-eslint/no-misused-promises": [ + "error", + { + checksVoidReturn: { attributes: false }, + }, + ], + }, + }, + ], +}; + +module.exports = config; diff --git a/config/eslint/next.js b/config/eslint/next.js new file mode 100644 index 00000000..7587867f --- /dev/null +++ b/config/eslint/next.js @@ -0,0 +1,6 @@ +/** @type {import('eslint').Linter.Config} */ +const config = { + extends: ["plugin:@next/next/recommended"], +}; + +module.exports = config; diff --git a/config/eslint/package.json b/config/eslint/package.json new file mode 100644 index 00000000..b4929cdd --- /dev/null +++ b/config/eslint/package.json @@ -0,0 +1,19 @@ +{ + "name": "eslint-config-libsqlstudio", + "private": true, + "version": "1.0.0", + "files": [ + "./base.js", + "./next.js", + "./react.js" + ], + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.6.0", + "@typescript-eslint/parser": "^7.6.0", + "eslint-config-next": "^14.1.4", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.0" + } +} diff --git a/config/eslint/react.js b/config/eslint/react.js new file mode 100644 index 00000000..ef33e317 --- /dev/null +++ b/config/eslint/react.js @@ -0,0 +1,25 @@ +/** @type {import('eslint').Linter.Config} */ +const config = { + overrides: [ + { + files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], + extends: [ + "plugin:react/recommended", + "plugin:react-hooks/recommended", + // "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + }, + rules: { + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "react/no-unknown-property": "warn" + }, + }, + ], +}; + +module.exports = config; diff --git a/config/eslint/tsconfig.json b/config/eslint/tsconfig.json new file mode 100644 index 00000000..747521af --- /dev/null +++ b/config/eslint/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": ["../tsconfig/base.json"], + "include": ["**/*.cjs"] +} diff --git a/config/tailwind/index.ts b/config/tailwind/index.ts new file mode 100644 index 00000000..8af504f0 --- /dev/null +++ b/config/tailwind/index.ts @@ -0,0 +1,93 @@ +import type { Config } from "tailwindcss"; + +export const LibSqlStudoTailwindPreset: Config = { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + shake: { + "10%, 90%": { + transform: "translate3d(-1px, 0, 0)", + }, + "20%, 80%": { + transform: "translate3d(2px, 0, 0)", + }, + "30%, 50%, 70%": { + transform: "translate3d(-4px, 0, 0)", + }, + "40%, 60%": { + transform: "translate3d(4px, 0, 0)", + }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + shake: "shake 0.82s cubic-bezier(.36,.07,.19,.97) both", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +}; diff --git a/config/tailwind/package.json b/config/tailwind/package.json new file mode 100644 index 00000000..a6846b71 --- /dev/null +++ b/config/tailwind/package.json @@ -0,0 +1,17 @@ +{ + "name": "@libsqlstudio/tailwind", + "private": true, + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "types": "./index.ts", + "default": "./index.ts" + }, + "./css": "./style.css" + }, + "devDependencies": { + "tailwindcss": "^3.4.3", + "tailwindcss-animate": "^1.0.7" + } +} diff --git a/src/app/globals.css b/config/tailwind/style.css similarity index 100% rename from src/app/globals.css rename to config/tailwind/style.css diff --git a/config/tailwind/tsconfig.json b/config/tailwind/tsconfig.json new file mode 100644 index 00000000..95c79a0f --- /dev/null +++ b/config/tailwind/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": ["../tsconfig/base.json"], + "include": ["**/*.ts"] +} diff --git a/config/tsconfig/base.json b/config/tsconfig/base.json new file mode 100644 index 00000000..60502896 --- /dev/null +++ b/config/tsconfig/base.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Strictest", + "compilerOptions": { + "strict": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + + "isolatedModules": true, + + "allowJs": true, + "checkJs": true, + + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + "lib": ["es2023"], + "module": "esnext", + "target": "es2022", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "noEmit": true, + "incremental": true + } +} diff --git a/config/tsconfig/next.json b/config/tsconfig/next.json new file mode 100644 index 00000000..789e42e8 --- /dev/null +++ b/config/tsconfig/next.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Next.js", + "extends": "./base.json", + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + } +} diff --git a/config/tsconfig/package.json b/config/tsconfig/package.json new file mode 100644 index 00000000..b444629a --- /dev/null +++ b/config/tsconfig/package.json @@ -0,0 +1,11 @@ +{ + "name": "@libsqlstudio/tsconfig", + "private": true, + "version": "1.0.0", + "type": "module", + "files": [ + "./base.json", + "./next.json", + "./react.json" + ] +} diff --git a/config/tsconfig/react.json b/config/tsconfig/react.json new file mode 100644 index 00000000..b8d0747d --- /dev/null +++ b/config/tsconfig/react.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "React", + "extends": "./base.json", + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx" + } +} diff --git a/gui/.eslintrc.cjs b/gui/.eslintrc.cjs new file mode 100644 index 00000000..7f0e3719 --- /dev/null +++ b/gui/.eslintrc.cjs @@ -0,0 +1,29 @@ +/** + * @type {import('eslint').Linter.Config} + */ +module.exports = { + root: true, + extends: ["libsqlstudio/base", "libsqlstudio/react"], + rules: { + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/consistent-type-imports": "off", + "@typescript-eslint/no-unnecessary-condition": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/prefer-includes": "off", + "@typescript-eslint/prefer-promise-reject-errors": "off", + "@typescript-eslint/no-confusing-void-expression": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/no-base-to-string": "off", + "@typescript-eslint/use-unknown-in-catch-callback-variable": "off", + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-dynamic-delete": "off", + "@typescript-eslint/non-nullable-type-assertion-style": "off", + "@typescript-eslint/array-type": "off", + "eslint-comments/no-unlimited-disable": "off", + "eslint-comments/disable-enable-pair": "off", + }, +}; diff --git a/gui/.gitignore b/gui/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/gui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/gui/jest.config.cjs b/gui/jest.config.cjs new file mode 100644 index 00000000..fe74d3f8 --- /dev/null +++ b/gui/jest.config.cjs @@ -0,0 +1,9 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "jsdom", + rootDir: "./", + moduleNameMapper: { + "^@gui/(.*)$": "/src/$1", + }, +}; diff --git a/gui/package.json b/gui/package.json new file mode 100644 index 00000000..90a478bd --- /dev/null +++ b/gui/package.json @@ -0,0 +1,98 @@ +{ + "name": "@libsqlstudio/gui", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./css": "./dist/index.css" + }, + "scripts": { + "dev": "tsup --watch", + "build": "tsup", + "staged": "pnpm run typecheck && pnpm run format && pnpm run lint && pnpm run test", + "typecheck": "tsc --noEmit", + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --check .", + "format:fix": "prettier --write ." + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "dependencies": { + "@blocknote/core": "^0.12.1", + "@blocknote/react": "^0.12.2", + "@codemirror/commands": "^6.3.3", + "@codemirror/lang-sql": "^6.5.5", + "@codemirror/view": "^6.26.3", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@justmiracle/result": "^1.2.0", + "@lezer/common": "^1.2.1", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.4.0", + "@libsql/client": "^0.5.3", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-context-menu": "^2.1.5", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-hover-card": "^1.0.7", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-menubar": "^1.0.4", + "@radix-ui/react-navigation-menu": "^1.1.4", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toggle": "^1.0.3", + "@radix-ui/react-toggle-group": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", + "@tiptap/core": "^2.3.0", + "@tiptap/react": "^2.3.0", + "@uiw/codemirror-extensions-langs": "^4.21.24", + "@uiw/codemirror-themes": "^4.21.21", + "@uiw/react-codemirror": "^4.21.21", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "cmdk": "^0.2.0", + "cookies-next": "^4.1.1", + "deep-equal": "^2.2.3", + "lucide-react": "^0.309.0", + "react-resizable-panels": "^1.0.9", + "sonner": "^1.4.41", + "sql-query-identifier": "^2.6.0", + "tailwind-merge": "^2.2.2" + }, + "devDependencies": { + "@libsqlstudio/tailwind": "workspace:*", + "@libsqlstudio/tsconfig": "workspace:*", + "@types/deep-equal": "^1.0.4", + "@types/jest": "^29.5.11", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "eslint-config-libsqlstudio": "workspace:*", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "ts-jest": "^29.1.2", + "tsup": "^8.0.2", + "vite": "^5.2.0", + "vite-tsconfig-paths": "^4.3.2" + } +} diff --git a/gui/postcss.config.js b/gui/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/gui/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/components/block-editor/editor.tsx b/gui/src/components/block-editor/editor.tsx similarity index 83% rename from src/components/block-editor/editor.tsx rename to gui/src/components/block-editor/editor.tsx index 50515098..38f6699f 100644 --- a/src/components/block-editor/editor.tsx +++ b/gui/src/components/block-editor/editor.tsx @@ -6,9 +6,10 @@ import "./style.css"; import { BlockNoteView } from "@blocknote/react"; import { SuggestionMenu } from "./suggestions"; import { BlockNoteEditor } from "@blocknote/core"; -import { useTheme } from "@/context/theme-provider"; +import { useTheme } from "@gui/contexts/theme-provider"; export interface BlockEditorProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any editor: BlockNoteEditor; } diff --git a/src/components/block-editor/extensions/code-block.tsx b/gui/src/components/block-editor/extensions/code-block.tsx similarity index 94% rename from src/components/block-editor/extensions/code-block.tsx rename to gui/src/components/block-editor/extensions/code-block.tsx index 44849d1b..5efb10e5 100644 --- a/src/components/block-editor/extensions/code-block.tsx +++ b/gui/src/components/block-editor/extensions/code-block.tsx @@ -1,17 +1,17 @@ -import { Button } from "@/components/ui/button"; +import { Button } from "@gui/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, -} from "@/components/ui/command"; +} from "@gui/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, -} from "@/components/ui/popover"; -import { cn, scoped } from "@/lib/utils"; +} from "@gui/components/ui/popover"; +import { cn, scoped } from "@gui/lib/utils"; import { createReactBlockSpec } from "../utils/create-block-spec"; import ReactCodeMirror from "@uiw/react-codemirror"; import { Check, ChevronsUpDown } from "lucide-react"; @@ -43,6 +43,7 @@ export const CodeBlock = createReactBlockSpec( : "plaintext"; const code = ""; + // eslint-disable-next-line @typescript-eslint/no-explicit-any (chain() as any).BNUpdateBlock(state.selection.from, { type: "codeBlock", props: { language, code }, @@ -93,7 +94,7 @@ export const CodeBlock = createReactBlockSpec( ); }, - }, + } ); interface LanguageSelectorProps { @@ -133,7 +134,7 @@ function LanguageSelector({ value, onValueChange }: LanguageSelectorProps) { {lang} diff --git a/src/components/block-editor/extensions/file-upload.ts b/gui/src/components/block-editor/extensions/file-upload.ts similarity index 80% rename from src/components/block-editor/extensions/file-upload.ts rename to gui/src/components/block-editor/extensions/file-upload.ts index e8a45363..41d0f950 100644 --- a/src/components/block-editor/extensions/file-upload.ts +++ b/gui/src/components/block-editor/extensions/file-upload.ts @@ -1,4 +1,4 @@ -import { uploadFile as uploadUserFile } from "@/lib/file-upload"; +import { uploadFile as uploadUserFile } from "@gui/lib/file-upload"; import { toast } from "sonner"; export async function uploadFile(file: File) { diff --git a/src/components/block-editor/extensions/index.ts b/gui/src/components/block-editor/extensions/index.ts similarity index 90% rename from src/components/block-editor/extensions/index.ts rename to gui/src/components/block-editor/extensions/index.ts index 890493b8..024ef7bb 100644 --- a/src/components/block-editor/extensions/index.ts +++ b/gui/src/components/block-editor/extensions/index.ts @@ -6,8 +6,8 @@ export { uploadFile } from "./file-upload"; export const schema = BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, - codeBlock: CodeBlock + codeBlock: CodeBlock, }, -}) +}); export type Block = typeof schema.Block; diff --git a/src/components/block-editor/index.tsx b/gui/src/components/block-editor/index.tsx similarity index 85% rename from src/components/block-editor/index.tsx rename to gui/src/components/block-editor/index.tsx index 1df5b5ba..dc81f300 100644 --- a/src/components/block-editor/index.tsx +++ b/gui/src/components/block-editor/index.tsx @@ -6,6 +6,7 @@ export const CONTENT_FORMAT = { BLOCK_NOTE: "BLOCK_NOTE", } as const; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export interface BlockContent { format: "BLOCK_NOTE"; content: TContent[] | undefined; diff --git a/src/components/block-editor/sheet.tsx b/gui/src/components/block-editor/sheet.tsx similarity index 96% rename from src/components/block-editor/sheet.tsx rename to gui/src/components/block-editor/sheet.tsx index e8d05d2a..3e6b3e17 100644 --- a/src/components/block-editor/sheet.tsx +++ b/gui/src/components/block-editor/sheet.tsx @@ -9,7 +9,7 @@ import { Separator } from "../ui/separator"; import { ScrollArea } from "../ui/scroll-area"; import { BlockEditor } from "./editor"; import { BlockContent, CONTENT_FORMAT } from "."; -import { BlockEditorConfigs } from "@/context/block-editor-provider"; +import { BlockEditorConfigs } from "@gui/contexts/block-editor-provider"; export interface BlockEditorSheetProps { configs: BlockEditorConfigs | null; diff --git a/src/components/block-editor/style.css b/gui/src/components/block-editor/style.css similarity index 100% rename from src/components/block-editor/style.css rename to gui/src/components/block-editor/style.css diff --git a/src/components/block-editor/suggestions/index.tsx b/gui/src/components/block-editor/suggestions/index.tsx similarity index 83% rename from src/components/block-editor/suggestions/index.tsx rename to gui/src/components/block-editor/suggestions/index.tsx index d7ab52f3..4c4892ab 100644 --- a/src/components/block-editor/suggestions/index.tsx +++ b/gui/src/components/block-editor/suggestions/index.tsx @@ -1,14 +1,10 @@ -import { - BlockNoteEditor, - filterSuggestionItems, -} from "@blocknote/core"; +import { BlockNoteEditor, filterSuggestionItems } from "@blocknote/core"; import { SuggestionMenuController, getDefaultReactSlashMenuItems, } from "@blocknote/react"; import { insertCodeBlock } from "./insert-code-block"; - // How we want to display the suggestions group // lower number will be displayed first const GROUP_ORDER = { @@ -22,6 +18,7 @@ const GROUP_ORDER = { const CUSTOM_SUGGESTIONS = [insertCodeBlock]; // utility function to get all custom suggestions +// eslint-disable-next-line @typescript-eslint/no-explicit-any function getCustomSlashMenuItems(editor: BlockNoteEditor) { return CUSTOM_SUGGESTIONS.map((item) => item(editor)); } @@ -29,6 +26,7 @@ function getCustomSlashMenuItems(editor: BlockNoteEditor) { export const SuggestionMenu = ({ editor, }: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any editor: BlockNoteEditor; }) => { return ( @@ -42,7 +40,7 @@ export const SuggestionMenu = ({ ]; // sort the suggestions by group - const sortedItems = allItem.toSorted((a, b) => { + const sortedItems = allItem.sort((a, b) => { const aOrder = GROUP_ORDER[a.group || ""] || 5; const bOrder = GROUP_ORDER[b.group || ""] || 5; return aOrder - bOrder; diff --git a/src/components/block-editor/suggestions/insert-code-block.tsx b/gui/src/components/block-editor/suggestions/insert-code-block.tsx similarity index 100% rename from src/components/block-editor/suggestions/insert-code-block.tsx rename to gui/src/components/block-editor/suggestions/insert-code-block.tsx diff --git a/src/components/block-editor/utils/create-block-spec.tsx b/gui/src/components/block-editor/utils/create-block-spec.tsx similarity index 92% rename from src/components/block-editor/utils/create-block-spec.tsx rename to gui/src/components/block-editor/utils/create-block-spec.tsx index 2d0f0dbe..01274c46 100644 --- a/src/components/block-editor/utils/create-block-spec.tsx +++ b/gui/src/components/block-editor/utils/create-block-spec.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ import { BlockFromConfig, BlockNoteEditor, @@ -45,7 +46,7 @@ export type ReactCustomBlockImplementation< contentRef: (node: HTMLElement | null) => void; }>; parse?: ( - el: HTMLElement, + el: HTMLElement ) => PartialBlockFromConfig["props"] | undefined; addInputRules?: () // block: BlockFromConfig, // editor: BlockNoteEditor, I, S> @@ -71,13 +72,13 @@ export function BlockContentWrapper< // Adds custom HTML attributes {...Object.fromEntries( Object.entries(props.domAttributes || {}).filter( - ([key]) => key !== "class", - ), + ([key]) => key !== "class" + ) )} // Sets blockContent class className={mergeCSSClasses( "bn-block-content", - props.domAttributes?.class || "", + props.domAttributes!["class"] || "" )} // Sets content type attribute data-content-type={props.blockType} @@ -90,11 +91,11 @@ export function BlockContentWrapper< .filter( ([prop, value]) => !inheritedProps.includes(prop) && - value !== props.propSchema[prop].default, + value !== props.propSchema[prop]!.default ) .map(([prop, value]) => { return [camelToDataKebab(prop), value]; - }), + }) )} > {props.children} @@ -110,7 +111,7 @@ export function createReactBlockSpec< const S extends StyleSchema, >( blockConfig: T, - blockImplementation: ReactCustomBlockImplementation, + blockImplementation: ReactCustomBlockImplementation ) { const node = createStronglyTypedTiptapNode({ name: blockConfig.type as T["type"], @@ -128,6 +129,7 @@ export function createReactBlockSpec< return blockImplementation.addInputRules?.() || []; }, + // @ts-expect-error parseHTML() { return getParseRules(blockConfig, blockImplementation.parse); }, @@ -154,7 +156,7 @@ export function createReactBlockSpec< props.getPos, editor, this.editor, - blockConfig.type, + blockConfig.type ) as any; // Gets the custom HTML attributes for `blockContent` nodes const blockContentDOMAttributes = @@ -181,7 +183,7 @@ export function createReactBlockSpec< }, { className: "bn-react-node-view-renderer", - }, + } )(props); }, }); @@ -208,11 +210,14 @@ export function createReactBlockSpec< /> ), - editor, + editor ); output.contentDOM?.setAttribute("data-editable", ""); - return output; + return output as { + dom: HTMLElement; + contentDOM: HTMLElement | undefined; + }; }, toExternalHTML: (block, editor) => { const blockContentDOMAttributes = @@ -238,7 +243,10 @@ export function createReactBlockSpec< }, editor); output.contentDOM?.setAttribute("data-editable", ""); - return output; + return output as { + dom: HTMLElement; + contentDOM: HTMLElement | undefined; + }; }, }); } diff --git a/src/components/block-editor/utils/react-render.tsx b/gui/src/components/block-editor/utils/react-render.tsx similarity index 85% rename from src/components/block-editor/utils/react-render.tsx rename to gui/src/components/block-editor/utils/react-render.tsx index d88cd863..74100a62 100644 --- a/src/components/block-editor/utils/react-render.tsx +++ b/gui/src/components/block-editor/utils/react-render.tsx @@ -4,6 +4,7 @@ import { Root, createRoot } from "react-dom/client"; export function renderToDOMSpec( fc: (refCB: (ref: HTMLElement | null) => void) => React.ReactNode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any editor: BlockNoteEditor | undefined ) { let contentDOM: HTMLElement | undefined; @@ -15,7 +16,7 @@ export function renderToDOMSpec( // This is currently only used for Styles. In this case, react context etc. won't be available inside `fc` root = createRoot(div); flushSync(() => { - root!.render(fc((el) => (contentDOM = el || undefined))); + root?.render(fc((el) => (contentDOM = el || undefined))); }); } else { // Render temporarily using `EditorContent` (which is stored somewhat hacky on `editor._tiptapEditor.contentComponent`) @@ -37,10 +38,8 @@ export function renderToDOMSpec( // clone so we can unmount the react root contentDOM?.setAttribute("data-tmp-find", "true"); const cloneRoot = div.cloneNode(true) as HTMLElement; - const dom = cloneRoot.firstElementChild! as HTMLElement; - const contentDOMClone = cloneRoot.querySelector( - "[data-tmp-find]" - ) as HTMLElement | null; + const dom = cloneRoot.firstElementChild as HTMLElement; + const contentDOMClone = cloneRoot.querySelector("[data-tmp-find]"); contentDOMClone?.removeAttribute("data-tmp-find"); root?.unmount(); diff --git a/src/components/code-preview/index.tsx b/gui/src/components/code-preview/index.tsx similarity index 96% rename from src/components/code-preview/index.tsx rename to gui/src/components/code-preview/index.tsx index caeb9197..a9526704 100644 --- a/src/components/code-preview/index.tsx +++ b/gui/src/components/code-preview/index.tsx @@ -1,7 +1,7 @@ -export default function CodePreview({ code }: { code: string }) { - return ( - -
{code}
-
- ); -} +export default function CodePreview({ code }: { code: string }) { + return ( + +
{code}
+
+ ); +} diff --git a/src/components/connection-dialog.tsx b/gui/src/components/connection-dialog.tsx similarity index 66% rename from src/components/connection-dialog.tsx rename to gui/src/components/connection-dialog.tsx index 6f758a0f..f342e019 100644 --- a/src/components/connection-dialog.tsx +++ b/gui/src/components/connection-dialog.tsx @@ -1,7 +1,6 @@ -import { Button } from "@/components/ui/button"; -import { useConnectionConfig } from "@/context/connection-config-provider"; -import { useRouter } from "next/navigation"; +import { Button } from "@gui/components/ui/button"; import LogoLoading from "./logo-loading"; +import { useConfig } from "@gui/contexts/config-provider"; export default function ConnectingDialog({ message, @@ -11,14 +10,11 @@ export default function ConnectingDialog({ message?: string; onRetry?: () => void; }>) { - const { config } = useConnectionConfig(); - - const router = useRouter(); - + const { name, onBack } = useConfig(); let body = (

- Connecting to {config.name} + Connecting to {name}

); @@ -34,7 +30,7 @@ export default function ConnectingDialog({

-
diff --git a/src/components/context-menu-handler.tsx b/gui/src/components/context-menu-handler.tsx similarity index 93% rename from src/components/context-menu-handler.tsx rename to gui/src/components/context-menu-handler.tsx index 2d6bb2be..9d05f8cb 100644 --- a/src/components/context-menu-handler.tsx +++ b/gui/src/components/context-menu-handler.tsx @@ -1,9 +1,9 @@ -import useMessageListener from "@/hooks/useMessageListener"; -import { MessageChannelName } from "@/messages/const"; -import { +import useMessageListener from "@gui/hooks/useMessageListener"; +import { MessageChannelName } from "@gui/messages/const"; +import type { OpenContextMenuList, OpenContextMenuOptions, -} from "@/messages/openContextMenu"; +} from "@gui/messages/open-context-menu"; import { ContextMenu, ContextMenuTrigger, @@ -15,7 +15,7 @@ import { ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent, -} from "@/components/ui/context-menu"; +} from "@gui/components/ui/context-menu"; import { useEffect, useRef, useState } from "react"; function ContextMenuList({ menu }: { menu: OpenContextMenuList }) { diff --git a/src/components/custom/ErrorMessage.tsx b/gui/src/components/custom/ErrorMessage.tsx similarity index 95% rename from src/components/custom/ErrorMessage.tsx rename to gui/src/components/custom/ErrorMessage.tsx index 43f6520b..3dfbec23 100644 --- a/src/components/custom/ErrorMessage.tsx +++ b/gui/src/components/custom/ErrorMessage.tsx @@ -1,7 +1,7 @@ -export default function ErrorMessage({ - message, -}: { - readonly message: string; -}) { - return
{message}
; -} +export default function ErrorMessage({ + message, +}: { + readonly message: string; +}) { + return
{message}
; +} diff --git a/src/components/database-gui.tsx b/gui/src/components/database-gui.tsx similarity index 83% rename from src/components/database-gui.tsx rename to gui/src/components/database-gui.tsx index 9cfd4be4..d65a88f5 100644 --- a/src/components/database-gui.tsx +++ b/gui/src/components/database-gui.tsx @@ -1,85 +1,85 @@ -"use client"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@/components/ui/resizable"; -import { useEffect, useMemo, useState } from "react"; -import WindowTabs, { WindowTabItemProps } from "./windows-tab"; -import useMessageListener from "@/hooks/useMessageListener"; -import { MessageChannelName } from "@/messages/const"; -import { OpenTabsProps, receiveOpenTabMessage } from "@/messages/open-tab"; -import QueryWindow from "@/components/tabs/query-tab"; -import { LucideCode, LucideDatabase, LucideSettings } from "lucide-react"; -import SidebarTab, { SidebarTabItem } from "./sidebar-tab"; -import SchemaView from "./schema-sidebar"; -import SettingSidebar from "./sidebar/setting-sidebar"; -import { useDatabaseDriver } from "@/context/DatabaseDriverProvider"; - -export default function DatabaseGui() { - const DEFAULT_WIDTH = 300; - - const [defaultWidthPercentage, setDefaultWidthPercentage] = useState(20); - - useEffect(() => { - setDefaultWidthPercentage((DEFAULT_WIDTH / window.innerWidth) * 100); - }, []); - - const { collaborationDriver } = useDatabaseDriver(); - const [selectedTabIndex, setSelectedTabIndex] = useState(0); - const [tabs, setTabs] = useState(() => [ - { - title: "Query", - key: "query", - component: , - icon: LucideCode, - }, - ]); - - useMessageListener( - MessageChannelName.OPEN_NEW_TAB, - (newTab) => { - if (newTab) { - receiveOpenTabMessage({ newTab, setSelectedTabIndex, setTabs }); - } - } - ); - - const sidebarTabs = useMemo(() => { - return [ - { - key: "database", - name: "Database", - content: , - icon: LucideDatabase, - }, - collaborationDriver - ? { - key: "setting", - name: "Setting", - content: , - icon: LucideSettings, - } - : undefined, - ].filter(Boolean) as SidebarTabItem[]; - }, [collaborationDriver]); - - return ( -
- - - - - - - - - -
- ); -} +"use client"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@gui/components/ui/resizable"; +import { useEffect, useMemo, useState } from "react"; +import WindowTabs, { WindowTabItemProps } from "./windows-tab"; +import useMessageListener from "@gui/hooks/useMessageListener"; +import { MessageChannelName } from "@gui/messages/const"; +import { OpenTabsProps, receiveOpenTabMessage } from "@gui/messages/open-tab"; +import QueryWindow from "@gui/components/tabs/query-tab"; +import { LucideCode, LucideDatabase, LucideSettings } from "lucide-react"; +import SidebarTab, { SidebarTabItem } from "./sidebar-tab"; +import SchemaView from "./schema-sidebar"; +import SettingSidebar from "./sidebar/setting-sidebar"; +import { useDatabaseDriver } from "@gui/contexts/driver-provider"; + +export default function DatabaseGui() { + const DEFAULT_WIDTH = 300; + + const [defaultWidthPercentage, setDefaultWidthPercentage] = useState(20); + + useEffect(() => { + setDefaultWidthPercentage((DEFAULT_WIDTH / window.innerWidth) * 100); + }, []); + + const { collaborationDriver } = useDatabaseDriver(); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + const [tabs, setTabs] = useState(() => [ + { + title: "Query", + key: "query", + component: , + icon: LucideCode, + }, + ]); + + useMessageListener( + MessageChannelName.OPEN_NEW_TAB, + (newTab) => { + if (newTab) { + receiveOpenTabMessage({ newTab, setSelectedTabIndex, setTabs }); + } + } + ); + + const sidebarTabs = useMemo(() => { + return [ + { + key: "database", + name: "Database", + content: , + icon: LucideDatabase, + }, + collaborationDriver + ? { + key: "setting", + name: "Setting", + content: , + icon: LucideSettings, + } + : undefined, + ].filter(Boolean) as SidebarTabItem[]; + }, [collaborationDriver]); + + return ( +
+ + + + + + + + + +
+ ); +} diff --git a/src/components/list-button-item.tsx b/gui/src/components/list-button-item.tsx similarity index 94% rename from src/components/list-button-item.tsx rename to gui/src/components/list-button-item.tsx index 4a47508d..399cf311 100644 --- a/src/components/list-button-item.tsx +++ b/gui/src/components/list-button-item.tsx @@ -1,5 +1,5 @@ import { buttonVariants } from "./ui/button"; -import { cn } from "@/lib/utils"; +import { cn } from "@gui/lib/utils"; import { LucideIcon } from "lucide-react"; export default function ListButtonItem({ diff --git a/src/components/loading-opacity.tsx b/gui/src/components/loading-opacity.tsx similarity index 100% rename from src/components/loading-opacity.tsx rename to gui/src/components/loading-opacity.tsx diff --git a/src/components/logo-loading.tsx b/gui/src/components/logo-loading.tsx similarity index 96% rename from src/components/logo-loading.tsx rename to gui/src/components/logo-loading.tsx index 62e123b9..a2545b5f 100644 --- a/src/components/logo-loading.tsx +++ b/gui/src/components/logo-loading.tsx @@ -1,21 +1,21 @@ -export default function LogoLoading() { - return ( -
-
- - ✱ - -
- -
-

LibSQL Studio

-
-
- ); -} +export default function LogoLoading() { + return ( +
+
+ + ✱ + +
+ +
+

LibSQL Studio

+
+
+ ); +} diff --git a/src/components/main-connection.tsx b/gui/src/components/main-connection.tsx similarity index 60% rename from src/components/main-connection.tsx rename to gui/src/components/main-connection.tsx index c98014b7..b3cc3512 100644 --- a/src/components/main-connection.tsx +++ b/gui/src/components/main-connection.tsx @@ -1,71 +1,64 @@ -"use client"; -import { useEffect, useLayoutEffect } from "react"; -import DatabaseGui from "./database-gui"; -import { useDatabaseDriver } from "@/context/DatabaseDriverProvider"; -import { TooltipProvider } from "@/components/ui/tooltip"; -import { AutoCompleteProvider } from "@/context/AutoCompleteProvider"; -import ContextMenuHandler from "./context-menu-handler"; -import InternalPubSub from "@/lib/internal-pubsub"; -import { useRouter } from "next/navigation"; -import { SchemaProvider } from "@/context/SchemaProvider"; -import { BlockEditorProvider } from "@/context/block-editor-provider"; -import { useConnectionConfig } from "@/context/connection-config-provider"; - -export interface ConnectionCredential { - url: string; - token: string; -} - -function MainConnection() { - const { databaseDriver: driver } = useDatabaseDriver(); - - useEffect(() => { - return () => { - driver.close(); - }; - }, [driver]); - - return ( - - - - - - ); -} - -function MainConnectionContainer() { - const router = useRouter(); - const { databaseDriver: driver } = useDatabaseDriver(); - const { config } = useConnectionConfig(); - - /** - * We use useLayoutEffect because it executes before - * other component mount. Since we need to attach the - * message handler first before children component - * start listening and sending message to each other - */ - useLayoutEffect(() => { - console.info("Injecting message into window object"); - window.internalPubSub = new InternalPubSub(); - }, [driver, router]); - - useEffect(() => { - document.title = config.name + " - LibSQL Studio"; - }, [config]); - - return ( - <> - - - - - - - - ); -} - -export default function MainScreen() { - return ; -} +"use client"; +import { useEffect, useLayoutEffect } from "react"; +import { TooltipProvider } from "@gui/components/ui/tooltip"; +import ContextMenuHandler from "./context-menu-handler"; +import { useDatabaseDriver } from "@gui/contexts/driver-provider"; +import DatabaseGui from "./database-gui"; +import { useConfig } from "@gui/contexts/config-provider"; +import { AutoCompleteProvider } from "@gui/contexts/auto-complete-provider"; +import { BlockEditorProvider } from "@gui/contexts/block-editor-provider"; +import InternalPubSub from "@gui/lib/internal-pubsub"; +import { SchemaProvider } from "@gui/contexts/schema-provider"; + +function MainConnection() { + const { databaseDriver: driver } = useDatabaseDriver(); + + useEffect(() => { + return () => { + driver.close(); + }; + }, [driver]); + + return ( + + + + + + ); +} + +function MainConnectionContainer() { + const { databaseDriver: driver } = useDatabaseDriver(); + const { name } = useConfig(); + + /** + * We use useLayoutEffect because it executes before + * other component mount. Since we need to attach the + * message handler first before children component + * start listening and sending message to each other + */ + useLayoutEffect(() => { + console.info("Injecting message into window object"); + window.internalPubSub = new InternalPubSub(); + }, [driver]); + + useEffect(() => { + document.title = name + " - LibSQL Studio"; + }, [name]); + + return ( + <> + + + + + + + + ); +} + +export default function MainScreen() { + return ; +} diff --git a/src/components/query-progress-log.tsx b/gui/src/components/query-progress-log.tsx similarity index 93% rename from src/components/query-progress-log.tsx rename to gui/src/components/query-progress-log.tsx index 626a2b0f..b6f6aff6 100644 --- a/src/components/query-progress-log.tsx +++ b/gui/src/components/query-progress-log.tsx @@ -1,85 +1,85 @@ -import { MultipleQueryProgress } from "@/lib/multiple-query"; -import { useEffect, useState } from "react"; -import CodePreview from "./code-preview"; - -function formatTimeAgo(ms: number) { - if (ms < 1000) { - return `${ms}ms`; - } else { - return `${(ms / 1000).toLocaleString(undefined, { - maximumFractionDigits: 2, - minimumFractionDigits: 2, - })}s`; - } -} - -export default function QueryProgressLog({ - progress, -}: { - progress: MultipleQueryProgress; -}) { - const [, setCurrentTime] = useState(() => Date.now()); - - useEffect(() => { - if (!progress.error) { - const intervalId = setInterval(() => setCurrentTime(Date.now()), 200); - return () => clearInterval(intervalId); - } - }, [progress]); - - const last3 = progress.logs.slice(-3).reverse(); - const value = progress.progress; - const total = progress.total; - const isEnded = total === value || !!progress.error; - - return ( -
-
- {isEnded ? ( - - Executed {value}/{total} - - ) : ( - - Executing {value}/{total} - - )} -
- -
- {last3.map((detail) => { - return ( -
- {detail.end && !detail.error && ( -
- [Query #{detail.order + 1}] This query took{" "} - {formatTimeAgo(detail.end - detail.start)} and affected{" "} - {detail.affectedRow} row(s). -
- )} - - {!!detail.error && ( -
-
{detail.error}
-
- )} - - {!detail.end && ( -
- Executing this query  - - {formatTimeAgo(Date.now() - detail.start)} - {" "} - ago. -
- )} - -
- -
- ); - })} -
-
- ); -} +import { MultipleQueryProgress } from "@gui/lib/multiple-query"; +import { useEffect, useState } from "react"; +import CodePreview from "./code-preview"; + +function formatTimeAgo(ms: number) { + if (ms < 1000) { + return `${ms}ms`; + } else { + return `${(ms / 1000).toLocaleString(undefined, { + maximumFractionDigits: 2, + minimumFractionDigits: 2, + })}s`; + } +} + +export default function QueryProgressLog({ + progress, +}: { + progress: MultipleQueryProgress; +}) { + const [, setCurrentTime] = useState(() => Date.now()); + + useEffect(() => { + if (!progress.error) { + const intervalId = setInterval(() => setCurrentTime(Date.now()), 200); + return () => clearInterval(intervalId); + } + }, [progress]); + + const last3 = progress.logs.slice(-3).reverse(); + const value = progress.progress; + const total = progress.total; + const isEnded = total === value || !!progress.error; + + return ( +
+
+ {isEnded ? ( + + Executed {value}/{total} + + ) : ( + + Executing {value}/{total} + + )} +
+ +
+ {last3.map((detail) => { + return ( +
+ {detail.end && !detail.error && ( +
+ [Query #{detail.order + 1}] This query took{" "} + {formatTimeAgo(detail.end - detail.start)} and affected{" "} + {detail.affectedRow} row(s). +
+ )} + + {!!detail.error && ( +
+
{detail.error}
+
+ )} + + {!detail.end && ( +
+ Executing this query  + + {formatTimeAgo(Date.now() - detail.start)} + {" "} + ago. +
+ )} + +
+ +
+ ); + })} +
+
+ ); +} diff --git a/src/components/query-result-table.tsx b/gui/src/components/query-result-table.tsx similarity index 91% rename from src/components/query-result-table.tsx rename to gui/src/components/query-result-table.tsx index 33260762..75044841 100644 --- a/src/components/query-result-table.tsx +++ b/gui/src/components/query-result-table.tsx @@ -1,484 +1,495 @@ -import GenericCell from "@/components/table-cell/GenericCell"; -import NumberCell from "@/components/table-cell/NumberCell"; -import TextCell from "@/components/table-cell/TextCell"; -import OptimizeTable, { - OptimizeTableCellRenderProps, - OptimizeTableHeaderWithIndexProps, -} from "@/components/table-optimized"; -import OptimizeTableState from "@/components/table-optimized/OptimizeTableState"; -import { exportRowsToExcel, exportRowsToSqlInsert } from "@/lib/export-helper"; -import { KEY_BINDING } from "@/lib/key-matcher"; -import { openContextMenuFromEvent } from "@/messages/openContextMenu"; -import { - LucideChevronDown, - LucidePin, - LucidePlus, - LucideSortAsc, - LucideSortDesc, - LucideTrash2, -} from "lucide-react"; -import React, { PropsWithChildren, useCallback, useState } from "react"; -import { - ColumnSortOption, - DatabaseValue, - TableColumnDataType, -} from "@/drivers/base-driver"; -import { useBlockEditor } from "@/context/block-editor-provider"; -import parseSafeJson from "@/lib/json-safe"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, - DropdownMenuItem, - DropdownMenuSeparator, -} from "./ui/dropdown-menu"; -import { triggerSelectFiles, uploadFile } from "@/lib/file-upload"; -import { toast } from "sonner"; -import { useDatabaseDriver } from "@/context/DatabaseDriverProvider"; -import BigNumberCell from "./table-cell/BigNumberCell"; - -interface ResultTableProps { - data: OptimizeTableState; - tableName?: string; - onSortColumnChange?: (columns: ColumnSortOption[]) => void; - sortColumns?: ColumnSortOption[]; -} - -function isBlockNoteString(value: DatabaseValue): boolean { - if (typeof value !== "string") return false; - if (!(value.startsWith("{") && value.endsWith("}"))) return false; - - const parsedJson = parseSafeJson(value, null); - if (!parsedJson) return false; - - return parsedJson?.format === "BLOCK_NOTE"; -} - -function Header({ - children, - header, -}: PropsWithChildren<{ header: OptimizeTableHeaderWithIndexProps }>) { - const [open, setOpen] = useState(false); - - return ( - - -
{ - setOpen(true); - }} - > - {header.icon ?
{header.icon}
: null} -
{header.displayName}
- -
-
- - {children} - -
- ); -} - -export default function ResultTable({ - data, - tableName, - onSortColumnChange, -}: ResultTableProps) { - const [stickyHeaderIndex, setStickHeaderIndex] = useState(); - const { openBlockEditor } = useBlockEditor(); - const { databaseDriver } = useDatabaseDriver(); - - const renderHeader = useCallback( - (header: OptimizeTableHeaderWithIndexProps) => { - return ( -
- { - setStickHeaderIndex( - header.index === stickyHeaderIndex ? undefined : header.index - ); - }} - > - - Pin Header - - - { - if (onSortColumnChange) { - onSortColumnChange([{ columnName: header.name, by: "ASC" }]); - } - }} - > - - Sort A → Z - - { - if (onSortColumnChange) { - onSortColumnChange([{ columnName: header.name, by: "DESC" }]); - } - }} - > - - Sort Z → A - -
- ); - }, - [stickyHeaderIndex, tableName, onSortColumnChange] - ); - - const renderCell = useCallback( - ({ y, x, state, header }: OptimizeTableCellRenderProps) => { - const isFocus = state.hasFocus(y, x); - const editMode = isFocus && state.isInEditMode(); - - if (header.dataType === TableColumnDataType.TEXT) { - const value = state.getValue(y, x) as DatabaseValue; - let editor: "input" | "blocknote" = "input"; // this is default editor - - if (isBlockNoteString(value)) { - editor = "blocknote"; - } - - return ( - } - focus={isFocus} - isChanged={state.hasCellChange(y, x)} - onChange={(newValue) => { - state.changeValue(y, x, newValue); - }} - /> - ); - } else if (header.dataType === TableColumnDataType.REAL) { - return ( - } - focus={isFocus} - isChanged={state.hasCellChange(y, x)} - onChange={(newValue) => { - state.changeValue(y, x, newValue); - }} - /> - ); - } else if (header.dataType === TableColumnDataType.INTEGER) { - if (databaseDriver.supportBigInt()) { - return ( - } - focus={isFocus} - isChanged={state.hasCellChange(y, x)} - onChange={(newValue) => { - state.changeValue(y, x, newValue); - }} - /> - ); - } else { - return ( - } - focus={isFocus} - isChanged={state.hasCellChange(y, x)} - onChange={(newValue) => { - state.changeValue(y, x, newValue); - }} - /> - ); - } - } - - return ; - }, - [databaseDriver] - ); - - const onHeaderContextMenu = useCallback((e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - }, []); - - const copyCallback = useCallback((state: OptimizeTableState) => { - const focus = state.getFocus(); - if (focus) { - const y = focus.y; - const x = focus.x; - window.navigator.clipboard.writeText(state.getValue(y, x) as string); - } - }, []); - - const pasteCallback = useCallback((state: OptimizeTableState) => { - const focus = state.getFocus(); - if (focus) { - const y = focus.y; - const x = focus.x; - window.navigator.clipboard.readText().then((pasteValue) => { - state.changeValue(y, x, pasteValue); - }); - } - }, []); - - const onCellContextMenu = useCallback( - ({ - state, - event, - }: { - state: OptimizeTableState; - event: React.MouseEvent; - }) => { - const randomUUID = crypto.randomUUID(); - const timestamp = Math.floor(Date.now() / 1000).toString(); - const hasFocus = !!state.getFocus(); - - function setFocusValue(newValue: unknown) { - const focusCell = state.getFocus(); - if (focusCell) { - state.changeValue(focusCell.y, focusCell.x, newValue); - } - } - - function getFocusValue() { - const focusCell = state.getFocus(); - if (focusCell) { - return state.getValue(focusCell.y, focusCell.x); - } - } - - openContextMenuFromEvent([ - { - title: "Insert Value", - disabled: !hasFocus, - subWidth: 200, - sub: [ - { - title:
NULL
, - onClick: () => { - setFocusValue(null); - }, - }, - { - title:
DEFAULT
, - onClick: () => { - setFocusValue(undefined); - }, - }, - { separator: true }, - { - title: ( -
- Unix Timestamp - {timestamp} -
- ), - onClick: () => { - setFocusValue(timestamp); - }, - }, - { separator: true }, - { - title: ( -
- UUID - {randomUUID} -
- ), - onClick: () => { - setFocusValue(randomUUID); - }, - }, - ], - }, - { - title: "Edit with Block Editor", - onClick: () => { - openBlockEditor({ - initialContent: getFocusValue() as string, - onSave: setFocusValue, - }); - }, - }, - - { - title: "Upload File", - onClick: async () => { - const files = await triggerSelectFiles(); - - if (files.error) return toast.error(files.error.message); - - const file = files.value[0]; - - const toastId = toast.loading("Uploading file..."); - const { data, error } = await uploadFile(file); - if (error) - return toast.error("Upload failed!", { - id: toastId, - description: error.message, - }); - - setFocusValue(data.url); - return toast.success("File uploaded!", { id: toastId }); - }, - }, - - { - separator: true, - }, - { - title: "Copy Cell Value", - shortcut: KEY_BINDING.copy.toString(), - onClick: () => { - copyCallback(state); - }, - }, - { - title: "Paste", - shortcut: KEY_BINDING.paste.toString(), - onClick: () => { - pasteCallback(state); - }, - }, - { - separator: true, - }, - { - title: "Copy Row As", - sub: [ - { - title: "Copy as Excel", - onClick: () => { - if (state.getSelectedRowCount() > 0) { - window.navigator.clipboard.writeText( - exportRowsToExcel(state.getSelectedRowsArray()) - ); - } - }, - }, - { - title: "Copy as INSERT SQL", - onClick: () => { - const headers = state - .getHeaders() - .map((column) => column?.name ?? ""); - - if (state.getSelectedRowCount() > 0) { - window.navigator.clipboard.writeText( - exportRowsToSqlInsert( - tableName ?? "UnknownTable", - headers, - state.getSelectedRowsArray() - ) - ); - } - }, - }, - ], - }, - { separator: true }, - { - title: "Insert row", - icon: LucidePlus, - onClick: () => { - data.insertNewRow(); - }, - }, - { - title: "Delete selected row(s)", - icon: LucideTrash2, - onClick: () => { - data.getSelectedRowIndex().forEach((index) => { - data.removeRow(index); - }); - }, - }, - ])(event); - }, - [data, tableName, copyCallback, pasteCallback, openBlockEditor] - ); - - const onKeyDown = useCallback( - (state: OptimizeTableState, e: React.KeyboardEvent) => { - if (state.isInEditMode()) return; - - if (KEY_BINDING.copy.match(e as React.KeyboardEvent)) { - copyCallback(state); - } else if ( - KEY_BINDING.paste.match(e as React.KeyboardEvent) - ) { - pasteCallback(state); - } else if (e.key === "ArrowRight") { - const focus = state.getFocus(); - if (focus && focus.x + 1 < state.getHeaderCount()) { - state.setFocus(focus.y, focus.x + 1); - state.scrollToFocusCell("right", "top"); - } - } else if (e.key === "ArrowLeft") { - const focus = state.getFocus(); - if (focus && focus.x - 1 >= 0) { - state.setFocus(focus.y, focus.x - 1); - state.scrollToFocusCell("left", "top"); - } - } else if (e.key === "ArrowUp") { - const focus = state.getFocus(); - if (focus && focus.y - 1 >= 0) { - state.setFocus(focus.y - 1, focus.x); - state.scrollToFocusCell("left", "top"); - } - } else if (e.key === "ArrowDown") { - const focus = state.getFocus(); - if (focus && focus.y + 1 < state.getRowsCount()) { - state.setFocus(focus.y + 1, focus.x); - state.scrollToFocusCell("left", "bottom"); - } - } else if (e.key === "Tab") { - const focus = state.getFocus(); - if (focus) { - const colCount = state.getHeaderCount(); - const n = focus.y * colCount + focus.x + 1; - const x = n % colCount; - const y = Math.floor(n / colCount); - if (y >= state.getRowsCount()) return; - state.setFocus(y, x); - state.scrollToFocusCell(x === 0 ? "left" : "right", "bottom"); - } - } else if (e.key === "Enter") { - state.enterEditMode(); - } - - e.preventDefault(); - }, - [copyCallback, pasteCallback] - ); - - return ( - - ); -} +import GenericCell from "@gui/components/table-cell/GenericCell"; +import NumberCell from "@gui/components/table-cell/NumberCell"; +import TextCell from "@gui/components/table-cell/TextCell"; +import OptimizeTable, { + OptimizeTableCellRenderProps, + OptimizeTableHeaderWithIndexProps, +} from "@gui/components/table-optimized"; +import OptimizeTableState from "@gui/components/table-optimized/OptimizeTableState"; +import { + exportRowsToExcel, + exportRowsToSqlInsert, +} from "@gui/lib/export-helper"; +import { KEY_BINDING } from "@gui/lib/key-matcher"; +import { openContextMenuFromEvent } from "@gui/messages/open-context-menu"; +import { + LucideChevronDown, + LucidePin, + LucidePlus, + LucideSortAsc, + LucideSortDesc, + LucideTrash2, +} from "lucide-react"; +import React, { PropsWithChildren, useCallback, useState } from "react"; +import { + ColumnSortOption, + DatabaseValue, + TableColumnDataType, +} from "@gui/drivers/base-driver"; +import { useBlockEditor } from "@gui/contexts/block-editor-provider"; +import parseSafeJson from "@gui/lib/json-safe"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + DropdownMenuItem, + DropdownMenuSeparator, +} from "./ui/dropdown-menu"; +import { triggerSelectFiles, uploadFile } from "@gui/lib/file-upload"; +import { toast } from "sonner"; +import BigNumberCell from "./table-cell/BigNumberCell"; +import { useDatabaseDriver } from "@gui/contexts/driver-provider"; + +interface ResultTableProps { + data: OptimizeTableState; + tableName?: string; + onSortColumnChange?: (columns: ColumnSortOption[]) => void; + sortColumns?: ColumnSortOption[]; +} + +function isBlockNoteString(value: DatabaseValue): boolean { + if (typeof value !== "string") return false; + if (!(value.startsWith("{") && value.endsWith("}"))) return false; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parsedJson = parseSafeJson(value, null); + if (!parsedJson) return false; + + return parsedJson?.format === "BLOCK_NOTE"; +} + +function Header({ + children, + header, +}: PropsWithChildren<{ header: OptimizeTableHeaderWithIndexProps }>) { + const [open, setOpen] = useState(false); + + return ( + + +
{ + setOpen(true); + }} + > + {header.icon ?
{header.icon}
: null} +
{header.displayName}
+ +
+
+ + {children} + +
+ ); +} + +export default function ResultTable({ + data, + tableName, + onSortColumnChange, +}: ResultTableProps) { + const [stickyHeaderIndex, setStickHeaderIndex] = useState(); + const { openBlockEditor } = useBlockEditor(); + const { databaseDriver } = useDatabaseDriver(); + + console.log("henlooooo"); + + const renderHeader = useCallback( + (header: OptimizeTableHeaderWithIndexProps) => { + return ( +
+ { + setStickHeaderIndex( + header.index === stickyHeaderIndex ? undefined : header.index + ); + }} + > + + Pin Header + + + { + if (onSortColumnChange) { + onSortColumnChange([{ columnName: header.name, by: "ASC" }]); + } + }} + > + + Sort A → Z + + { + if (onSortColumnChange) { + onSortColumnChange([{ columnName: header.name, by: "DESC" }]); + } + }} + > + + Sort Z → A + +
+ ); + }, + [stickyHeaderIndex, tableName, onSortColumnChange] + ); + + const renderCell = useCallback( + ({ y, x, state, header }: OptimizeTableCellRenderProps) => { + const isFocus = state.hasFocus(y, x); + const editMode = isFocus && state.isInEditMode(); + + if (header.dataType === TableColumnDataType.TEXT) { + const value = state.getValue(y, x) as DatabaseValue; + let editor: "input" | "blocknote" = "input"; // this is default editor + + if (isBlockNoteString(value)) { + editor = "blocknote"; + } + + return ( + } + focus={isFocus} + isChanged={state.hasCellChange(y, x)} + onChange={(newValue) => { + state.changeValue(y, x, newValue); + }} + /> + ); + } else if (header.dataType === TableColumnDataType.REAL) { + return ( + } + focus={isFocus} + isChanged={state.hasCellChange(y, x)} + onChange={(newValue) => { + state.changeValue(y, x, newValue); + }} + /> + ); + } else if (header.dataType === TableColumnDataType.INTEGER) { + if (databaseDriver.supportBigInt()) { + return ( + } + focus={isFocus} + isChanged={state.hasCellChange(y, x)} + onChange={(newValue) => { + state.changeValue(y, x, newValue); + }} + /> + ); + } else { + return ( + } + focus={isFocus} + isChanged={state.hasCellChange(y, x)} + onChange={(newValue) => { + state.changeValue(y, x, newValue); + }} + /> + ); + } + } + + return ; + }, + [databaseDriver] + ); + + const onHeaderContextMenu = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const copyCallback = useCallback((state: OptimizeTableState) => { + const focus = state.getFocus(); + if (focus) { + const y = focus.y; + const x = focus.x; + window.navigator.clipboard.writeText(state.getValue(y, x) as string); + } + }, []); + + const pasteCallback = useCallback((state: OptimizeTableState) => { + const focus = state.getFocus(); + if (focus) { + const y = focus.y; + const x = focus.x; + window.navigator.clipboard.readText().then((pasteValue) => { + state.changeValue(y, x, pasteValue); + }); + } + }, []); + + const onCellContextMenu = useCallback( + ({ + state, + event, + }: { + state: OptimizeTableState; + event: React.MouseEvent; + }) => { + const randomUUID = crypto.randomUUID(); + const timestamp = Math.floor(Date.now() / 1000).toString(); + const hasFocus = !!state.getFocus(); + + function setFocusValue(newValue: unknown) { + const focusCell = state.getFocus(); + if (focusCell) { + state.changeValue(focusCell.y, focusCell.x, newValue); + } + } + + function getFocusValue() { + const focusCell = state.getFocus(); + if (focusCell) { + return state.getValue(focusCell.y, focusCell.x); + } + + return undefined; + } + + openContextMenuFromEvent([ + { + title: "Insert Value", + disabled: !hasFocus, + subWidth: 200, + sub: [ + { + title:
NULL
, + onClick: () => { + setFocusValue(null); + }, + }, + { + title:
DEFAULT
, + onClick: () => { + setFocusValue(undefined); + }, + }, + { separator: true }, + { + title: ( +
+ Unix Timestamp + {timestamp} +
+ ), + onClick: () => { + setFocusValue(timestamp); + }, + }, + { separator: true }, + { + title: ( +
+ UUID + {randomUUID} +
+ ), + onClick: () => { + setFocusValue(randomUUID); + }, + }, + ], + }, + { + title: "Edit with Block Editor", + onClick: () => { + openBlockEditor({ + initialContent: getFocusValue() as string, + onSave: setFocusValue, + }); + }, + }, + + { + title: "Upload File", + onClick: async () => { + const files = await triggerSelectFiles(); + + if (files.error) return toast.error(files.error.message); + + const file = files.value[0]; + if (!file) return; + + const toastId = toast.loading("Uploading file..."); + const { data, error } = await uploadFile(file); + if (error) + return toast.error("Upload failed!", { + id: toastId, + description: error.message, + }); + + setFocusValue(data.url); + return toast.success("File uploaded!", { id: toastId }); + }, + }, + + { + separator: true, + }, + { + title: "Copy Cell Value", + shortcut: KEY_BINDING.copy.toString(), + onClick: () => { + copyCallback(state); + }, + }, + { + title: "Paste", + shortcut: KEY_BINDING.paste.toString(), + onClick: () => { + pasteCallback(state); + }, + }, + { + separator: true, + }, + { + title: "Copy Row As", + sub: [ + { + title: "Copy as Excel", + onClick: () => { + if (state.getSelectedRowCount() > 0) { + window.navigator.clipboard.writeText( + exportRowsToExcel(state.getSelectedRowsArray()) + ); + } + }, + }, + { + title: "Copy as INSERT SQL", + onClick: () => { + const headers = state + .getHeaders() + .map((column) => column?.name ?? ""); + + if (state.getSelectedRowCount() > 0) { + window.navigator.clipboard.writeText( + exportRowsToSqlInsert( + tableName ?? "UnknownTable", + headers, + state.getSelectedRowsArray() + ) + ); + } + }, + }, + ], + }, + { separator: true }, + { + title: "Insert row", + icon: LucidePlus, + onClick: () => { + data.insertNewRow(); + }, + }, + { + title: "Delete selected row(s)", + icon: LucideTrash2, + onClick: () => { + data.getSelectedRowIndex().forEach((index) => { + data.removeRow(index); + }); + }, + }, + ])(event); + }, + [data, tableName, copyCallback, pasteCallback, openBlockEditor] + ); + + const onKeyDown = useCallback( + (state: OptimizeTableState, e: React.KeyboardEvent) => { + if (state.isInEditMode()) return; + + if (KEY_BINDING.copy.match(e as React.KeyboardEvent)) { + copyCallback(state); + } else if ( + KEY_BINDING.paste.match(e as React.KeyboardEvent) + ) { + pasteCallback(state); + } else if (e.key === "ArrowRight") { + const focus = state.getFocus(); + if (focus && focus.x + 1 < state.getHeaderCount()) { + state.setFocus(focus.y, focus.x + 1); + state.scrollToFocusCell("right", "top"); + } + } else if (e.key === "ArrowLeft") { + const focus = state.getFocus(); + if (focus && focus.x - 1 >= 0) { + state.setFocus(focus.y, focus.x - 1); + state.scrollToFocusCell("left", "top"); + } + } else if (e.key === "ArrowUp") { + const focus = state.getFocus(); + if (focus && focus.y - 1 >= 0) { + state.setFocus(focus.y - 1, focus.x); + state.scrollToFocusCell("left", "top"); + } + } else if (e.key === "ArrowDown") { + const focus = state.getFocus(); + if (focus && focus.y + 1 < state.getRowsCount()) { + state.setFocus(focus.y + 1, focus.x); + state.scrollToFocusCell("left", "bottom"); + } + } else if (e.key === "Tab") { + const focus = state.getFocus(); + if (focus) { + const colCount = state.getHeaderCount(); + const n = focus.y * colCount + focus.x + 1; + const x = n % colCount; + const y = Math.floor(n / colCount); + if (y >= state.getRowsCount()) return; + state.setFocus(y, x); + state.scrollToFocusCell(x === 0 ? "left" : "right", "bottom"); + } + } else if (e.key === "Enter") { + state.enterEditMode(); + } + + e.preventDefault(); + }, + [copyCallback, pasteCallback] + ); + + console.log("Herer"); + + return ( + + ); +} diff --git a/src/components/schema-editor/column-check-popup.tsx b/gui/src/components/schema-editor/column-check-popup.tsx similarity index 92% rename from src/components/schema-editor/column-check-popup.tsx rename to gui/src/components/schema-editor/column-check-popup.tsx index ac64d5e7..05642151 100644 --- a/src/components/schema-editor/column-check-popup.tsx +++ b/gui/src/components/schema-editor/column-check-popup.tsx @@ -1,62 +1,62 @@ -import { DatabaseTableColumnConstraint } from "@/drivers/base-driver"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import { LucideCheck } from "lucide-react"; -import { Button } from "../ui/button"; -import { ColumnChangeEvent } from "./schema-editor-column-list"; -import { Textarea } from "../ui/textarea"; - -export default function ColumnCheckPopup({ - constraint, - disabled, - onChange, -}: Readonly<{ - constraint: DatabaseTableColumnConstraint; - disabled: boolean; - onChange: ColumnChangeEvent; -}>) { - return ( - - - - - - - -
-
Check
- -