From d8acdaf97d21ae77d4b7abadb8a1f176b503297b Mon Sep 17 00:00:00 2001 From: abearxiong Date: Thu, 1 Jan 2026 23:56:56 +0800 Subject: [PATCH] feat: enhance studio app with left panel toggle and loading state - Added a left panel toggle button in the header to show/hide the ViewList component. - Integrated loading state management in the studio store for better user feedback during data fetching. - Updated the ViewList component to utilize a dropdown menu for editing and deleting views. - Improved UI elements with consistent border styles and loading indicators. - Refactored the DataItemForm and ViewFormItem components for better user experience and added clipboard copy functionality. - Introduced a new DropdownMenu component for better dropdown handling across the application. --- web/package.json | 1 + web/pnpm-lock.yaml | 246 +++++++++++++++++ web/src/apps/studio/index.tsx | 41 ++- web/src/apps/studio/store.ts | 239 +++++++++------- web/src/apps/view/components/DataItemForm.tsx | 2 +- web/src/apps/view/components/ViewEditor.tsx | 2 +- web/src/apps/view/components/ViewFormItem.tsx | 14 +- web/src/apps/view/list.tsx | 160 ++++++----- web/src/components/ui/dropdown-menu.tsx | 255 ++++++++++++++++++ 9 files changed, 786 insertions(+), 174 deletions(-) create mode 100644 web/src/components/ui/dropdown-menu.tsx diff --git a/web/package.json b/web/package.json index d97bbe6..7d348ed 100644 --- a/web/package.json +++ b/web/package.json @@ -30,6 +30,7 @@ "@kevisual/registry": "^0.0.1", "@kevisual/router": "^0.0.52", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.18", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 896d84d..dffb1ba 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.3.3 version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-label': specifier: ^2.1.8 version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -560,6 +563,21 @@ packages: cpu: [x64] os: [win32] + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@handsontable/pikaday@1.0.0': resolution: {integrity: sha512-1VN6N38t5/DcjJ7y7XUYrDx1LuzvvzlrFdBdMG90Qo1xc8+LXHqbWbsTEm5Ec5gXTEbDEO53vUT35R+2COmOyg==} @@ -790,6 +808,19 @@ packages: '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-checkbox@1.3.3': resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} peerDependencies: @@ -803,6 +834,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -834,6 +878,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.11': resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} peerDependencies: @@ -847,6 +900,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.3': resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} peerDependencies: @@ -891,6 +957,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.9': resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: @@ -943,6 +1035,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -1015,6 +1120,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-size@1.1.1': resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} peerDependencies: @@ -1024,6 +1138,9 @@ packages: '@types/react': optional: true + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rc-component/async-validator@5.0.4': resolution: {integrity: sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==} engines: {node: '>=14.x'} @@ -4160,6 +4277,23 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@floating-ui/utils@0.2.10': {} + '@handsontable/pikaday@1.0.0': {} '@img/colour@1.0.0': @@ -4461,6 +4595,15 @@ snapshots: '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -4477,6 +4620,18 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 @@ -4511,6 +4666,12 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.7 + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -4524,6 +4685,21 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 @@ -4557,6 +4733,50 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -4595,6 +4815,23 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-slot@1.2.3(@types/react@19.2.7)(react@19.2.3)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) @@ -4649,6 +4886,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.7)(react@19.2.3)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.7 + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.3)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) @@ -4656,6 +4900,8 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/rect@1.1.1': {} + '@rc-component/async-validator@5.0.4': dependencies: '@babel/runtime': 7.28.4 diff --git a/web/src/apps/studio/index.tsx b/web/src/apps/studio/index.tsx index 2eed08a..ad0a93c 100644 --- a/web/src/apps/studio/index.tsx +++ b/web/src/apps/studio/index.tsx @@ -1,17 +1,24 @@ import { toast, ToastContainer } from 'react-toastify'; import { useStudioStore } from './store.ts'; import { useEffect, useState } from 'react'; -import { MonitorPlay, Play } from 'lucide-react'; +import { MonitorPlay, Play, PanelLeft, PanelLeftClose } from 'lucide-react'; import { Panel, Group } from 'react-resizable-panels' import { ViewList } from '../view/list.tsx'; +import { useShallow } from 'zustand/shallow'; + export const AppProvider = () => { + const { showLeftPanel } = useStudioStore(useShallow((state) => ({ + showLeftPanel: state.showLeftPanel, + }))); return
- + {showLeftPanel && - + } - + + + { theme="light" />
} - +export const WrapperHeader = (props: { children: React.ReactNode }) => { + const showLeftPanel = useStudioStore(state => state.showLeftPanel); + const setShowLeftPanel = useStudioStore(state => state.setShowLeftPanel); + return
+
+
{ + setShowLeftPanel(!showLeftPanel); + }}> + {showLeftPanel ? : } +
+
+
+ {props.children} +
+
+} interface RouteItem { id: string; path?: string; @@ -37,7 +59,7 @@ interface RouteItem { } export const App = () => { - const { routes, getRouteList, run } = useStudioStore(); + const { routes, getRouteList, run, loading } = useStudioStore(); const [expandedIds, setExpandedIds] = useState>(new Set()); const [visibleIds, setVisibleIds] = useState>(new Set()); @@ -68,7 +90,8 @@ export const App = () => { return (
-
+ {loading &&
加载中...
} +
{routes.map((route: RouteItem) => { const isExpanded = expandedIds.has(route.id); const isIdVisible = visibleIds.has(route.id); @@ -77,7 +100,7 @@ export const App = () => { return (
{/* ID and Path/Key in one line */} @@ -120,7 +143,7 @@ export const App = () => { className="cursor-pointer group" >
{isExpanded ? ( diff --git a/web/src/apps/studio/store.ts b/web/src/apps/studio/store.ts index b0d480e..3a5f92d 100644 --- a/web/src/apps/studio/store.ts +++ b/web/src/apps/studio/store.ts @@ -4,7 +4,13 @@ import { query } from '@/modules/query.ts' import { toast } from 'react-toastify'; import { use } from '@kevisual/context' import { MyCache } from '@kevisual/cache' +import { persist } from 'zustand/middleware'; +const historyReplace = (url: string) => { + if (window.history.replaceState) { + window.history.replaceState(null, '', url); + } +} type RouteItem = { id: string; path?: string; @@ -16,8 +22,10 @@ type RouteItem = { type RouteViewList = Array; interface StudioState { + loading: boolean; + setLoading: (loading: boolean) => void; routes: Array; - getRouteList: () => Promise; + getRouteList: (viewId?: string) => Promise; run: (route: RouteItem) => Promise; queryProxy?: QueryProxy; init: (force?: boolean) => Promise<{ queryProxy: QueryProxy }>; @@ -27,106 +35,145 @@ interface StudioState { updateRouteView: (view: RouterViewData) => Promise; deleteRouteView: (id: string) => Promise; currentView?: RouterViewData; + setCurrentView: (view?: RouterViewData) => Promise; + showLeftPanel: boolean; + setShowLeftPanel: (show: boolean) => void; } const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -export const useStudioStore = create((set, get) => ({ - routes: [], - getRouteList: async () => { - await get().getCurrentView(); - const state = await get().init(); - let queryProxy = state.queryProxy; - const url = new URL(window.location.href); - const viewId = url.searchParams.get('viewId') || 'default'; - const routes: any[] = await queryProxy.listRoutes(() => true, { viewId }); - set({ routes }); - get().getViewList(); - }, - getViewList: async () => { - const res = await query.post({ path: 'views', key: 'list' }); - if (res.code === 200) { - const list = res.data.list as RouteViewList || []; - set({ routeViewList: list }); - } - }, - getCurrentView: async () => { - const url = new URL(window.location.href); - const viewId = url.searchParams.get('viewId'); - if (!viewId) { - return; - } - const res = await query.post({ path: 'views', key: 'current', data: { viewId: viewId } }); - if (res.code === 200) { - const view = res.data as RouterViewData; - set({ currentView: view }); - } else { - set({ currentView: undefined }); - } - }, - routeViewList: [], - updateRouteView: async (view: RouterViewData) => { - const res = await query.post({ path: 'views', key: 'update', data: view }); - if (res.code !== 200) { - toast.error(`视图更新失败:${res.message || '未知错误'}`); - return; - } else { - get().getViewList(); - toast.success('视图更新成功'); - } - }, - deleteRouteView: async (id: string) => { - const res = await query.post({ path: 'views', key: 'delete', data: { id } }); - if (res.code !== 200) { - toast.error(`视图删除失败:${res.message || '未知错误'}`); - return; - } - get().getViewList(); - toast.success('视图删除成功'); - }, - run: async (route: RouteItem) => { - const state = await get().init(); - let queryProxy = state.queryProxy; - const res = await queryProxy.run({ path: route.path, key: route.key }); - if (res.code !== 200) { - toast.error(`运行失败:${res.message || '未知错误'}`); - } else if (res.code === 200) { - // - } - }, - queryProxy: undefined, - router: undefined, - init: async (force?: boolean) => { - // let _url = 'http://localhost:52002/api/router'; - let _url = 'http://localhost:52000/api/router'; - // let _url = '/api/router'; - let queryProxy = get().queryProxy; - if (queryProxy && !force) { - return { queryProxy }; - } - let currentView: RouterViewData | undefined = get().currentView; - console.log('currentView in init', currentView); - const routerViewData: RouterViewData = currentView || { - views: [{ - id: 'default', - title: '默认视图', - query: `WHERE path = 'file' ` - }], - data: { - items: [] +export const useStudioStore = create()( + persist( + (set, get) => ({ + loading: false, + setLoading: (loading: boolean) => set({ loading }), + routes: [], + getRouteList: async () => { + await get().getCurrentView(); + const state = await get().init(); + let currentView: RouterViewData | undefined = get().currentView; + let queryProxy = state.queryProxy; + const viewId = currentView?.viewId ?? '' + const routes: any[] = await queryProxy.listRoutes(() => true, { viewId }); + set({ routes }); }, - viewId: 'default', + setCurrentView: async (view?: RouterViewData) => { + const beforeView = get().currentView; + set({ currentView: view, routes: [] }); + const viewId = view?.viewId || ''; + const url = new URL(window.location.href); + if (viewId) { + url.searchParams.set('viewId', viewId); + } else { + url.searchParams.delete('viewId'); + } + historyReplace(url.toString()); + await get().init(beforeView?.id !== view?.id); + await get().getRouteList(); + }, + getViewList: async () => { + const res = await query.post({ path: 'views', key: 'list' }); + if (res.code === 200) { + const list = res.data.list as RouteViewList || []; + set({ routeViewList: list }); + } + }, + getCurrentView: async () => { + const url = new URL(window.location.href); + const viewId = url.searchParams.get('viewId'); + if (!viewId) { + return; + } + const res = await query.post({ path: 'views', key: 'current', data: { viewId: viewId } }); + if (res.code === 200) { + const view = res.data as RouterViewData; + view.viewId = viewId; + set({ currentView: view }); + } else { + set({ currentView: undefined }); + } + }, + routeViewList: [], + updateRouteView: async (view: RouterViewData) => { + const res = await query.post({ path: 'views', key: 'update', data: view }); + if (res.code !== 200) { + toast.error(`视图更新失败:${res.message || '未知错误'}`); + return; + } else { + get().getViewList(); + toast.success('视图更新成功'); + } + }, + deleteRouteView: async (id: string) => { + const res = await query.post({ path: 'views', key: 'delete', data: { id } }); + if (res.code !== 200) { + toast.error(`视图删除失败:${res.message || '未知错误'}`); + return; + } + get().getViewList(); + toast.success('视图删除成功'); + }, + run: async (route: RouteItem) => { + const state = await get().init(); + let queryProxy = state.queryProxy; + const res = await queryProxy.run({ path: route.path, key: route.key }); + if (res.code !== 200) { + toast.error(`运行失败:${res.message || '未知错误'}`); + } else if (res.code === 200) { + // + } + }, + queryProxy: undefined, + router: undefined, + init: async (force?: boolean) => { + // let _url = 'http://localhost:52002/api/router'; // Github starred + // let _url = 'http://localhost:52000/api/router'; // 浏览器 + // let _url = '/api/router'; + let queryProxy = get().queryProxy; + if (queryProxy && !force) { + return { queryProxy }; + } + let currentView: RouterViewData | undefined = get().currentView; + const routerViewData: RouterViewData = currentView || { + views: [{ + id: '', + title: '默认视图', + query: `` + }], + data: { + items: [ + { + title: '默认路由', + id: '', + description: '', + type: 'api', + api: { + url: '/api/router' + } + } + ] + }, + viewId: '', + } + queryProxy = new QueryProxy({ + routerViewData + }); + set({ loading: true }); + await queryProxy.init(); + await sleep(500); + set({ loading: false }); + set({ queryProxy }); + return { queryProxy } + }, + showLeftPanel: false, + setShowLeftPanel: (show: boolean) => set({ showLeftPanel: show }), + }), + { + name: 'studio-storage', + partialize: (state) => ({ showLeftPanel: state.showLeftPanel }), } - console.log('initializing query proxy with view', routerViewData); - queryProxy = new QueryProxy({ - routerViewData - }); - await queryProxy.init(); - await sleep(500); - set({ queryProxy }); - return { queryProxy } - }, -})); + ) +); use('studioStore', () => { return useStudioStore.getState(); -}); \ No newline at end of file +}); diff --git a/web/src/apps/view/components/DataItemForm.tsx b/web/src/apps/view/components/DataItemForm.tsx index 2dca55e..5ef1467 100644 --- a/web/src/apps/view/components/DataItemForm.tsx +++ b/web/src/apps/view/components/DataItemForm.tsx @@ -95,7 +95,7 @@ export const DataItemForm = ({ item, onChange, onRemove }: DataItemFormProps) => } return ( -
+

数据项配置

-
+
handleCopyId(view.id)}> void; onDelete: (id: string) => void }) => { + const [expanded, setExpanded] = useState(false); + const studioStore = useStudioStore(); + useEffect(() => { + const currentViewId = studioStore.currentView?.viewId; + if (view.views.some((v: any) => v.id === currentViewId)) { + setExpanded(true); + } + }, [studioStore.currentView?.viewId]); + const ShowViews = (props: { views: { id: string, title: string }[] }) => { + const studioStore = useStudioStore(); + const currentViewId = studioStore.currentView?.viewId; + const isActiveView = (viewId: string) => { + return viewId === currentViewId; + } + return
+ {props.views.map(v => ( +
{ + studioStore.setCurrentView({ ...view, viewId: v.id }) + }}> + {v.title || '未命名视图'} +
+ ))} +
+ } + return
+
setExpanded(!expanded)}> +
+ + {view.title || '未命名视图'} +
+ + + + + + + onEdit(view)}> + + 编辑 + + onDelete(view.id)} + className="cursor-pointer text-red-600 focus:text-red-600" + > + + 删除 + + + +
+ + {expanded && + + } +
+} export const ViewList = () => { - const { routeViewList, updateRouteView, deleteRouteView } = useStudioStore(); - const [selectedItems, setSelectedItems] = useState([]); + const { routeViewList, updateRouteView, deleteRouteView, getViewList } = useStudioStore(); const [searchTerm, setSearchTerm] = useState(""); const [editorOpen, setEditorOpen] = useState(false); const [editingView, setEditingView] = useState(null); @@ -24,10 +84,14 @@ export const ViewList = () => { (view.title || '未命名视图').toLowerCase().includes(searchTerm.toLowerCase()) || (view.description || '').toLowerCase().includes(searchTerm.toLowerCase()) ); + useEffect(() => { + getViewList(); + }, []) - - const handleRefresh = () => { - // 刷新逻辑 + const handleRefresh = async () => { + const toastId = toast.loading('正在刷新视图列表...'); + await getViewList(); + toast.update(toastId, { render: '视图列表已刷新', type: 'success', isLoading: false, autoClose: 1000 }); }; const handleAdd = () => { @@ -50,7 +114,7 @@ export const ViewList = () => { }; return ( -
+
{ />
- -
- - - - 视图名称 - - - - - {filteredViews.length === 0 ? ( - - - {searchTerm ? '未找到匹配的视图' : '暂无视图'} - - - ) : ( - filteredViews.map((view) => ( - - -
- - {view.title || '未命名视图'} -
-
- -
- - -
-
-
- )) - )} -
-
+
+ {filteredViews.length === 0 ? ( +
+ {searchTerm ? '未找到匹配的视图' : '暂无视图'} +
+ ) : ( + filteredViews.map((view) => ( + + )) + )} +
) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}