From b83143a22314aafa4736d5a40ac13d0b05d3890b Mon Sep 17 00:00:00 2001 From: nzaytsev Date: Fri, 17 Jan 2025 14:02:57 +0700 Subject: [PATCH 1/2] wip react 19 --- ThirdPartyNotices.txt | 8 +- package.json | 10 +- pnpm-lock.yaml | 432 ++++++++++-------- src/webviews/apps/plus/graph/GraphWrapper.tsx | 7 +- src/webviews/apps/plus/graph/graph.scss | 2 +- src/webviews/apps/plus/graph/graph.tsx | 14 +- 6 files changed, 257 insertions(+), 216 deletions(-) diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 3fc0b4ad3ac7e..3a89fcc45732e 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -28,8 +28,8 @@ This project incorporates components from the projects listed below. 23. node-fetch version 2.7.0 (https://github.com/bitinn/node-fetch) 24. os-browserify version 0.3.0 (https://github.com/CoderPuppy/os-browserify) 25. path-browserify version 1.0.1 (https://github.com/browserify/path-browserify) -26. react-dom version 16.8.4 (https://github.com/facebook/react) -27. react version 16.8.4 (https://github.com/facebook/react) +26. react-dom version 19.0.0 (https://github.com/facebook/react) +27. react version 19.0.0 (https://github.com/facebook/react) 28. signal-utils version 0.21.1 (https://github.com/proposal-signals/signal-utils) 29. slug version 10.0.0 (https://github.com/Trott/slug) 30. sortablejs version 1.15.0 (https://github.com/SortableJS/Sortable) @@ -2129,7 +2129,7 @@ END OF path-browserify NOTICES AND INFORMATION ========================================= MIT License -Copyright (c) Facebook, Inc. and its affiliates. +Copyright (c) Meta Platforms, Inc. and affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -2156,7 +2156,7 @@ END OF react-dom NOTICES AND INFORMATION ========================================= MIT License -Copyright (c) Facebook, Inc. and its affiliates. +Copyright (c) Meta Platforms, Inc. and affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package.json b/package.json index 60b6a016e45be..a31d7448ebc4a 100644 --- a/package.json +++ b/package.json @@ -20116,7 +20116,7 @@ "vscode:prepublish": "pnpm run bundle" }, "dependencies": { - "@gitkraken/gitkraken-components": "10.7.0", + "@gitkraken/gitkraken-components": "11.0.0-vnext", "@gitkraken/provider-apis": "0.26.1", "@gitkraken/shared-web-components": "0.1.1-rc.15", "@gk-nzaytsev/fast-string-truncated-width": "1.1.0", @@ -20143,8 +20143,8 @@ "node-fetch": "2.7.0", "os-browserify": "0.3.0", "path-browserify": "1.0.1", - "react": "16.8.4", - "react-dom": "16.8.4", + "react": "19.0.0", + "react-dom": "19.0.0", "signal-utils": "0.21.1", "slug": "10.0.0", "sortablejs": "1.15.0" @@ -20158,8 +20158,8 @@ "@types/eslint__js": "8.42.3", "@types/mocha": "10.0.10", "@types/node": "18.15.13", - "@types/react": "17.0.83", - "@types/react-dom": "17.0.25", + "@types/react": "19.0.6", + "@types/react-dom": "19.0.3", "@types/slug": "5.0.9", "@types/sortablejs": "1.15.8", "@types/vscode": "1.82.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8b47ab05627c..ee900bc397426 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: .: dependencies: '@gitkraken/gitkraken-components': - specifier: 10.7.0 - version: 10.7.0 + specifier: 11.0.0-vnext + version: 11.0.0-vnext(@types/react@19.0.6)(react@19.0.0) '@gitkraken/provider-apis': specifier: 0.26.1 version: 0.26.1(encoding@0.1.13) @@ -34,7 +34,7 @@ importers: version: 1.1.3 '@lit/react': specifier: 1.0.7 - version: 1.0.7(@types/react@17.0.83) + version: 1.0.7(@types/react@19.0.6) '@lit/task': specifier: 1.0.2 version: 1.0.2 @@ -67,7 +67,7 @@ importers: version: 1.28.0 '@shoelace-style/shoelace': specifier: 2.19.1 - version: 2.19.1(@floating-ui/utils@0.2.9)(@types/react@17.0.83) + version: 2.19.1(@floating-ui/utils@0.2.9)(@types/react@19.0.6) '@vscode/codicons': specifier: 0.0.36 version: 0.0.36 @@ -96,11 +96,11 @@ importers: specifier: 1.0.1 version: 1.0.1 react: - specifier: 16.8.4 - version: 16.8.4 + specifier: 19.0.0 + version: 19.0.0 react-dom: - specifier: 16.8.4 - version: 16.8.4(react@16.8.4) + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) signal-utils: specifier: 0.21.1 version: 0.21.1(signal-polyfill@0.2.2) @@ -113,7 +113,7 @@ importers: devDependencies: '@eamodio/eslint-lite-webpack-plugin': specifier: 0.2.0 - version: 0.2.0(@swc/core@1.10.4)(esbuild@0.24.2)(eslint@9.19.0(jiti@2.4.0))(webpack-cli@6.0.1)(webpack@5.97.1) + version: 0.2.0(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(eslint@9.19.0(jiti@2.4.0))(webpack-cli@6.0.1)(webpack@5.97.1) '@eslint/js': specifier: 9.19.0 version: 9.19.0 @@ -122,7 +122,7 @@ importers: version: 1.50.0 '@swc/core': specifier: 1.10.4 - version: 1.10.4 + version: 1.10.4(@swc/helpers@0.5.15) '@twbs/fantasticon': specifier: 3.0.0 version: 3.0.0 @@ -136,11 +136,11 @@ importers: specifier: 18.15.13 version: 18.15.13 '@types/react': - specifier: 17.0.83 - version: 17.0.83 + specifier: 19.0.6 + version: 19.0.6 '@types/react-dom': - specifier: 17.0.25 - version: 17.0.25 + specifier: 19.0.3 + version: 19.0.3(@types/react@19.0.6) '@types/slug': specifier: 5.0.9 version: 5.0.9 @@ -278,7 +278,7 @@ importers: version: 3.3.2 terser-webpack-plugin: specifier: 5.3.11 - version: 5.3.11(@swc/core@1.10.4)(esbuild@0.24.2)(webpack@5.97.1) + version: 5.3.11(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1) ts-loader: specifier: 9.5.2 version: 9.5.2(typescript@5.7.3)(webpack@5.97.1) @@ -290,7 +290,7 @@ importers: version: 8.21.0(eslint@9.19.0(jiti@2.4.0))(typescript@5.7.3) webpack: specifier: 5.97.1 - version: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + version: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) webpack-bundle-analyzer: specifier: 4.10.2 version: 4.10.2 @@ -368,8 +368,8 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/runtime-corejs2@7.26.7': - resolution: {integrity: sha512-C7fo97gUfsUP54j6GcQ+rJXyW6vgRRqF7J1ZxXesWcQtSRyzH1+eYrqFGzmU2JSUGFV0hQA2zLY/Z8AMrEx0qg==} + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} '@babel/runtime@7.26.7': @@ -606,8 +606,10 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - '@gitkraken/gitkraken-components@10.7.0': - resolution: {integrity: sha512-0oekeCgTgZNAtUFNH8eSIdOfPOVG3IUIXoaKuOBY0dRT6TLc5Q/ARyujdtWLHpdD3FC/GZv46N9IdQL4AEIwNA==} + '@gitkraken/gitkraken-components@11.0.0-vnext': + resolution: {integrity: sha512-xxSzjnAQLNL2Lax3mEuODEXALYAKH7R2R/F/RClHrlNeDNEf9CWJvy2JwFEYwylPQoKUwEi+k3kpa1M3h6EoTA==} + peerDependencies: + react: 19.0.0 '@gitkraken/provider-apis@0.26.1': resolution: {integrity: sha512-p4l2v/HLRXKlU3nBsr7jH4X8Zya+y/uPoGu8feX5DR+Oi73PyHcRqrFnzLMSY/ffMI2lj1AYPU+3cxmc8Gp41Q==} @@ -1072,6 +1074,9 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1102,6 +1107,28 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@react-aria/ssr@3.9.7': + resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@restart/hooks@0.4.16': + resolution: {integrity: sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==} + peerDependencies: + react: '>=16.8.0' + + '@restart/hooks@0.5.1': + resolution: {integrity: sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==} + peerDependencies: + react: '>=16.8.0' + + '@restart/ui@1.9.2': + resolution: {integrity: sha512-MWWqJqSyqUWWPBOOiRQrX57CBc/9CoYONg7sE+uag72GCAuYrHGU5c49vU5s4BUSBgiKNY6rL7TULqGDrouUaA==} + peerDependencies: + react: '>=16.14.0' + react-dom: '>=16.14.0' + '@shoelace-style/animations@1.2.0': resolution: {integrity: sha512-avvo1xxkLbv2dgtabdewBbqcJfV0e0zCwFqkPMnHFGbJbBHorRFfMAHh1NG9ymmXn0jW95ibUVH03E1NYXD6Gw==} @@ -1191,6 +1218,9 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/types@0.1.17': resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} @@ -1270,14 +1300,18 @@ packages: '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/react-dom@17.0.25': - resolution: {integrity: sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA==} + '@types/react-dom@19.0.3': + resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} + peerDependencies: + '@types/react': ^19.0.0 - '@types/react@17.0.83': - resolution: {integrity: sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw==} + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' - '@types/scheduler@0.16.8': - resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + '@types/react@19.0.6': + resolution: {integrity: sha512-gIlMztcTeDgXCUj0vCBOqEuSEhX//63fW9SZtCJ+agxoQTOklwDfiEMlTWn4mR/C/UK5VHlpwsCsOyf7/hc4lw==} '@types/slug@5.0.9': resolution: {integrity: sha512-6Yp8BSplP35Esa/wOG1wLNKiqXevpQTEF/RcL/NV6BBQaMmZh4YlDwCgrrFSoUE4xAGvnKd5c+lkQJmPrBAzfQ==} @@ -1291,6 +1325,9 @@ packages: '@types/vscode@1.82.0': resolution: {integrity: sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==} + '@types/warning@3.0.3': + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} + '@types/webpack@5.28.5': resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} @@ -2027,10 +2064,6 @@ packages: peerDependencies: webpack: ^5.1.0 - core-js@2.6.12: - resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} - deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2347,6 +2380,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -2379,9 +2416,6 @@ packages: dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} - dom-helpers@3.4.0: - resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==} - dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -3457,9 +3491,6 @@ packages: jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} - keycode@2.2.1: - resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==} - keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} @@ -4460,22 +4491,26 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - re-resizable@6.9.11: - resolution: {integrity: sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ==} + re-resizable@6.10.3: + resolution: {integrity: sha512-zvWb7X3RJMA4cuSrqoxgs3KR+D+pEXnGrD2FAD6BMYAULnZsSF4b7AOVyG6pC3VVNVOtlagGDCDmZSwWLjjBBw==} peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-bootstrap@0.32.4: - resolution: {integrity: sha512-xj+JfaPOvnvr3ow0aHC7Y3HaBKZNR1mm361hVxVzVX3fcdJNIrfiodbQ0m9nLBpNxiKG6FTU2lq/SbTDYT2vew==} + react-bootstrap@2.10.7: + resolution: {integrity: sha512-w6mWb3uytB5A18S2oTZpYghcOUK30neMBBvZ/bEfA+WIF2dF4OGqjzoFVMpVXBjtyf92gkmRToHlddiMAVhQqQ==} peerDependencies: - react: ^0.14.9 || >=15.3.0 - react-dom: ^0.14.9 || >=15.3.0 + '@types/react': '>=16.14.8' + react: '>=16.14.0' + react-dom: '>=16.14.0' + peerDependenciesMeta: + '@types/react': + optional: true - react-dom@16.8.4: - resolution: {integrity: sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ==} + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: - react: ^16.0.0 + react: ^19.0.0 react-dragula@1.1.17: resolution: {integrity: sha512-gJdY190sPWAyV8jz79vyK9SGk97bVOHjUguVNIYIEVosvt27HLxnbJo4qiuEkb/nAuGY13Im2CHup92fUyO3fw==} @@ -4486,31 +4521,20 @@ packages: react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} - react-onclickoutside@6.13.0: - resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} + react-onclickoutside@6.13.1: + resolution: {integrity: sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==} peerDependencies: react: ^15.5.x || ^16.x || ^17.x || ^18.x react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x - react-overlays@0.8.3: - resolution: {integrity: sha512-h6GT3jgy90PgctleP39Yu3eK1v9vaJAW73GOA/UbN9dJ7aAN4BTZD6793eI1D5U+ukMk17qiqN/wl3diK1Z5LA==} - peerDependencies: - react: ^0.14.9 || >=15.3.0 - react-dom: ^0.14.9 || >=15.3.0 - - react-prop-types@0.4.0: - resolution: {integrity: sha512-IyjsJhDX9JkoOV9wlmLaS7z+oxYoIWhfzDcFy7inwoAKTu+VcVNrVpPmLeioJ94y6GeDRsnwarG1py5qofFQMg==} + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: - react: '>=0.14.0' + react: '>=16.6.0' + react-dom: '>=16.6.0' - react-transition-group@2.9.0: - resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} - peerDependencies: - react: '>=15.0.0' - react-dom: '>=15.0.0' - - react@16.8.4: - resolution: {integrity: sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg==} + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} read-installed-packages@2.0.1: @@ -4822,8 +4846,8 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - scheduler@0.13.6: - resolution: {integrity: sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==} + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} @@ -5324,11 +5348,16 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - uncontrollable@5.1.0: - resolution: {integrity: sha512-5FXYaFANKaafg4IVZXUNtGyzsnYEvqlr9wQ3WpZxFpEUxl29A3H6Q4G1Dnnorvq9TGOGATBApWR4YpLAh+F5hw==} + uncontrollable@7.2.1: + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} peerDependencies: react: '>=15.0.0' + uncontrollable@8.0.4: + resolution: {integrity: sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==} + peerDependencies: + react: '>=16.14.0' + underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} @@ -5390,9 +5419,6 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - warning@3.0.0: - resolution: {integrity: sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==} - warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} @@ -5574,15 +5600,15 @@ packages: snapshots: - '@axosoft/react-virtualized@9.22.3-gitkraken.3(react-dom@16.8.4(react@16.8.4))(react@16.8.4)': + '@axosoft/react-virtualized@9.22.3-gitkraken.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.7 clsx: 1.2.1 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 16.8.4 - react-dom: 16.8.4(react@16.8.4) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) react-lifecycles-compat: 3.0.4 '@azure/abort-controller@2.1.2': @@ -5674,9 +5700,8 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': {} - '@babel/runtime-corejs2@7.26.7': + '@babel/runtime@7.26.0': dependencies: - core-js: 2.6.12 regenerator-runtime: 0.14.1 '@babel/runtime@7.26.7': @@ -5693,14 +5718,14 @@ snapshots: '@discoveryjs/json-ext@0.6.3': {} - '@eamodio/eslint-lite-webpack-plugin@0.2.0(@swc/core@1.10.4)(esbuild@0.24.2)(eslint@9.19.0(jiti@2.4.0))(webpack-cli@6.0.1)(webpack@5.97.1)': + '@eamodio/eslint-lite-webpack-plugin@0.2.0(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(eslint@9.19.0(jiti@2.4.0))(webpack-cli@6.0.1)(webpack@5.97.1)': dependencies: '@types/eslint': 9.6.1 - '@types/webpack': 5.28.5(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + '@types/webpack': 5.28.5(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) eslint: 9.19.0(jiti@2.4.0) fast-glob: 3.3.3 minimatch: 10.0.1 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -5853,16 +5878,18 @@ snapshots: '@gar/promisify@1.1.3': {} - '@gitkraken/gitkraken-components@10.7.0': + '@gitkraken/gitkraken-components@11.0.0-vnext(@types/react@19.0.6)(react@19.0.0)': dependencies: - '@axosoft/react-virtualized': 9.22.3-gitkraken.3(react-dom@16.8.4(react@16.8.4))(react@16.8.4) + '@axosoft/react-virtualized': 9.22.3-gitkraken.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) classnames: 2.5.1 - re-resizable: 6.9.11(react-dom@16.8.4(react@16.8.4))(react@16.8.4) - react: 16.8.4 - react-bootstrap: 0.32.4(react-dom@16.8.4(react@16.8.4))(react@16.8.4) - react-dom: 16.8.4(react@16.8.4) + re-resizable: 6.10.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-bootstrap: 2.10.7(@types/react@19.0.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react-dom: 19.0.0(react@19.0.0) react-dragula: 1.1.17 - react-onclickoutside: 6.13.0(react-dom@16.8.4(react@16.8.4))(react@16.8.4) + react-onclickoutside: 6.13.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + transitivePeerDependencies: + - '@types/react' '@gitkraken/provider-apis@0.26.1(encoding@0.1.13)': dependencies: @@ -6033,9 +6060,9 @@ snapshots: dependencies: '@lit/reactive-element': 2.0.4 - '@lit/react@1.0.7(@types/react@17.0.83)': + '@lit/react@1.0.7(@types/react@19.0.6)': dependencies: - '@types/react': 17.0.83 + '@types/react': 19.0.6 '@lit/reactive-element@2.0.4': dependencies: @@ -6281,6 +6308,8 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@popperjs/core@2.11.8': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -6304,15 +6333,44 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@react-aria/ssr@3.9.7(react@19.0.0)': + dependencies: + '@swc/helpers': 0.5.15 + react: 19.0.0 + + '@restart/hooks@0.4.16(react@19.0.0)': + dependencies: + dequal: 2.0.3 + react: 19.0.0 + + '@restart/hooks@0.5.1(react@19.0.0)': + dependencies: + dequal: 2.0.3 + react: 19.0.0 + + '@restart/ui@1.9.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.26.0 + '@popperjs/core': 2.11.8 + '@react-aria/ssr': 3.9.7(react@19.0.0) + '@restart/hooks': 0.5.1(react@19.0.0) + '@types/warning': 3.0.3 + dequal: 2.0.3 + dom-helpers: 5.2.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + uncontrollable: 8.0.4(react@19.0.0) + warning: 4.0.3 + '@shoelace-style/animations@1.2.0': {} '@shoelace-style/localize@3.2.1': {} - '@shoelace-style/shoelace@2.19.1(@floating-ui/utils@0.2.9)(@types/react@17.0.83)': + '@shoelace-style/shoelace@2.19.1(@floating-ui/utils@0.2.9)(@types/react@19.0.6)': dependencies: '@ctrl/tinycolor': 4.1.0 '@floating-ui/dom': 1.6.13 - '@lit/react': 1.0.7(@types/react@17.0.83) + '@lit/react': 1.0.7(@types/react@19.0.6) '@shoelace-style/animations': 1.2.0 '@shoelace-style/localize': 3.2.1 composed-offset-position: 0.0.6(@floating-ui/utils@0.2.9) @@ -6356,7 +6414,7 @@ snapshots: '@swc/core-win32-x64-msvc@1.10.4': optional: true - '@swc/core@1.10.4': + '@swc/core@1.10.4(@swc/helpers@0.5.15)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.17 @@ -6371,9 +6429,14 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.10.4 '@swc/core-win32-ia32-msvc': 1.10.4 '@swc/core-win32-x64-msvc': 1.10.4 + '@swc/helpers': 0.5.15 '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + '@swc/types@0.1.17': dependencies: '@swc/counter': 0.1.3 @@ -6463,17 +6526,17 @@ snapshots: '@types/prop-types@15.7.14': {} - '@types/react-dom@17.0.25': + '@types/react-dom@19.0.3(@types/react@19.0.6)': dependencies: - '@types/react': 17.0.83 + '@types/react': 19.0.6 - '@types/react@17.0.83': + '@types/react-transition-group@4.4.12(@types/react@19.0.6)': dependencies: - '@types/prop-types': 15.7.14 - '@types/scheduler': 0.16.8 - csstype: 3.1.3 + '@types/react': 19.0.6 - '@types/scheduler@0.16.8': {} + '@types/react@19.0.6': + dependencies: + csstype: 3.1.3 '@types/slug@5.0.9': {} @@ -6483,11 +6546,13 @@ snapshots: '@types/vscode@1.82.0': {} - '@types/webpack@5.28.5(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1)': + '@types/warning@3.0.3': {} + + '@types/webpack@5.28.5(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1)': dependencies: '@types/node': 18.15.13 tapable: 2.2.1 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -6770,17 +6835,17 @@ snapshots: '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.97.1)': dependencies: - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-bundle-analyzer@4.10.2)(webpack@5.97.1) '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.97.1)': dependencies: - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-bundle-analyzer@4.10.2)(webpack@5.97.1) '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.97.1)': dependencies: - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-bundle-analyzer@4.10.2)(webpack@5.97.1) '@xmldom/xmldom@0.7.13': {} @@ -7254,7 +7319,7 @@ snapshots: circular-dependency-plugin@5.2.2(webpack@5.97.1): dependencies: - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) classnames@2.5.1: {} @@ -7267,7 +7332,7 @@ snapshots: clean-webpack-plugin@4.0.0(webpack@5.97.1): dependencies: del: 4.1.1 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) cli-cursor@4.0.0: dependencies: @@ -7381,9 +7446,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.0 serialize-javascript: 6.0.2 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) - - core-js@2.6.12: {} + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) core-util-is@1.0.3: {} @@ -7411,7 +7474,7 @@ snapshots: cheerio: 1.0.0-rc.12 html-webpack-plugin: 5.6.3(webpack@5.97.1) lodash: 4.17.21 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) css-declaration-sorter@7.2.0(postcss@8.5.1): dependencies: @@ -7428,7 +7491,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.6.3 optionalDependencies: - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) css-minimizer-webpack-plugin@7.0.0(esbuild@0.24.2)(webpack@5.97.1): dependencies: @@ -7438,7 +7501,7 @@ snapshots: postcss: 8.5.1 schema-utils: 4.3.0 serialize-javascript: 6.0.2 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) optionalDependencies: esbuild: 0.24.2 @@ -7715,6 +7778,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-libc@1.0.3: @@ -7741,10 +7806,6 @@ snapshots: dependencies: utila: 0.4.0 - dom-helpers@3.4.0: - dependencies: - '@babel/runtime': 7.26.7 - dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.26.7 @@ -7947,7 +8008,7 @@ snapshots: esbuild: 0.24.2 get-tsconfig: 4.10.0 loader-utils: 2.0.4 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) webpack-sources: 1.4.3 esbuild-node-externals@1.16.0(esbuild@0.24.2): @@ -8015,7 +8076,7 @@ snapshots: optionalDependencies: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.21.0(eslint@9.19.0(jiti@2.4.0))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.0)) eslint-plugin-import-x: 4.6.1(eslint@9.19.0(jiti@2.4.0))(typescript@5.7.3) - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-bundle-analyzer@4.10.2)(webpack@5.97.1) webpack-merge: 6.0.1 @@ -8267,7 +8328,7 @@ snapshots: semver: 7.6.3 tapable: 2.2.1 typescript: 5.7.3 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) form-data@4.0.1: dependencies: @@ -8518,7 +8579,7 @@ snapshots: dependencies: html-minifier-terser: 7.2.0 parse5: 7.2.1 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) html-minifier-terser@6.1.0: dependencies: @@ -8548,7 +8609,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) htmlparser2@6.1.0: dependencies: @@ -8643,7 +8704,7 @@ snapshots: dependencies: schema-utils: 4.3.0 serialize-javascript: 6.0.2 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) optionalDependencies: sharp: 0.33.5 svgo: 3.3.2 @@ -9021,8 +9082,6 @@ snapshots: jwa: 2.0.0 safe-buffer: 5.2.1 - keycode@2.2.1: {} - keygrip@1.1.0: dependencies: tsscmp: 1.0.6 @@ -9325,7 +9384,7 @@ snapshots: dependencies: schema-utils: 4.3.0 tapable: 2.2.1 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) minimatch@10.0.1: dependencies: @@ -10046,9 +10105,9 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 - prop-types-extra@1.1.1(react@16.8.4): + prop-types-extra@1.1.1(react@19.0.0): dependencies: - react: 16.8.4 + react: 19.0.0 react-is: 16.13.1 warning: 4.0.3 @@ -10117,35 +10176,35 @@ snapshots: strip-json-comments: 2.0.1 optional: true - re-resizable@6.9.11(react-dom@16.8.4(react@16.8.4))(react@16.8.4): + re-resizable@6.10.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - react: 16.8.4 - react-dom: 16.8.4(react@16.8.4) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) - react-bootstrap@0.32.4(react-dom@16.8.4(react@16.8.4))(react@16.8.4): + react-bootstrap@2.10.7(@types/react@19.0.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@babel/runtime-corejs2': 7.26.7 + '@babel/runtime': 7.26.0 + '@restart/hooks': 0.4.16(react@19.0.0) + '@restart/ui': 1.9.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@types/prop-types': 15.7.14 + '@types/react-transition-group': 4.4.12(@types/react@19.0.6) classnames: 2.5.1 - dom-helpers: 3.4.0 + dom-helpers: 5.2.1 invariant: 2.2.4 - keycode: 2.2.1 prop-types: 15.8.1 - prop-types-extra: 1.1.1(react@16.8.4) - react: 16.8.4 - react-dom: 16.8.4(react@16.8.4) - react-overlays: 0.8.3(react-dom@16.8.4(react@16.8.4))(react@16.8.4) - react-prop-types: 0.4.0(react@16.8.4) - react-transition-group: 2.9.0(react-dom@16.8.4(react@16.8.4))(react@16.8.4) - uncontrollable: 5.1.0(react@16.8.4) - warning: 3.0.0 + prop-types-extra: 1.1.1(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + uncontrollable: 7.2.1(react@19.0.0) + warning: 4.0.3 + optionalDependencies: + '@types/react': 19.0.6 - react-dom@16.8.4(react@16.8.4): + react-dom@19.0.0(react@19.0.0): dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - react: 16.8.4 - scheduler: 0.13.6 + react: 19.0.0 + scheduler: 0.25.0 react-dragula@1.1.17: dependencies: @@ -10156,42 +10215,21 @@ snapshots: react-lifecycles-compat@3.0.4: {} - react-onclickoutside@6.13.0(react-dom@16.8.4(react@16.8.4))(react@16.8.4): - dependencies: - react: 16.8.4 - react-dom: 16.8.4(react@16.8.4) - - react-overlays@0.8.3(react-dom@16.8.4(react@16.8.4))(react@16.8.4): + react-onclickoutside@6.13.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - classnames: 2.5.1 - dom-helpers: 3.4.0 - prop-types: 15.8.1 - prop-types-extra: 1.1.1(react@16.8.4) - react: 16.8.4 - react-dom: 16.8.4(react@16.8.4) - react-transition-group: 2.9.0(react-dom@16.8.4(react@16.8.4))(react@16.8.4) - warning: 3.0.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) - react-prop-types@0.4.0(react@16.8.4): + react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - react: 16.8.4 - warning: 3.0.0 - - react-transition-group@2.9.0(react-dom@16.8.4(react@16.8.4))(react@16.8.4): - dependencies: - dom-helpers: 3.4.0 + '@babel/runtime': 7.26.0 + dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 16.8.4 - react-dom: 16.8.4(react@16.8.4) - react-lifecycles-compat: 3.0.4 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) - react@16.8.4: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - scheduler: 0.13.6 + react@19.0.0: {} read-installed-packages@2.0.1: dependencies: @@ -10469,7 +10507,7 @@ snapshots: optionalDependencies: sass: 1.83.4 sass-embedded: 1.77.8 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) sass@1.83.4: dependencies: @@ -10481,10 +10519,7 @@ snapshots: sax@1.4.1: {} - scheduler@0.13.6: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 + scheduler@0.25.0: {} schema-utils@3.3.0: dependencies: @@ -10901,16 +10936,16 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - terser-webpack-plugin@5.3.11(@swc/core@1.10.4)(esbuild@0.24.2)(webpack@5.97.1): + terser-webpack-plugin@5.3.11(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.0 serialize-javascript: 6.0.2 terser: 5.37.0 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) optionalDependencies: - '@swc/core': 1.10.4 + '@swc/core': 1.10.4(@swc/helpers@0.5.15) esbuild: 0.24.2 terser@5.37.0: @@ -10965,7 +11000,7 @@ snapshots: semver: 7.6.3 source-map: 0.7.4 typescript: 5.7.3 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) tsconfig-paths@3.15.0: dependencies: @@ -11086,10 +11121,17 @@ snapshots: which-boxed-primitive: 1.1.1 optional: true - uncontrollable@5.1.0(react@16.8.4): + uncontrollable@7.2.1(react@19.0.0): dependencies: + '@babel/runtime': 7.26.0 + '@types/react': 19.0.6 invariant: 2.2.4 - react: 16.8.4 + react: 19.0.0 + react-lifecycles-compat: 3.0.4 + + uncontrollable@8.0.4(react@19.0.0): + dependencies: + react: 19.0.0 underscore@1.13.7: {} @@ -11142,10 +11184,6 @@ snapshots: vscode-uri@3.0.8: {} - warning@3.0.0: - dependencies: - loose-envify: 1.4.0 - warning@4.0.3: dependencies: loose-envify: 1.4.0 @@ -11189,7 +11227,7 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1) + webpack: 5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1) webpack-merge: 6.0.1 optionalDependencies: webpack-bundle-analyzer: 4.10.2 @@ -11213,7 +11251,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.97.1(@swc/core@1.10.4)(esbuild@0.24.2)(webpack-cli@6.0.1): + webpack@5.97.1(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@6.0.1): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.6 @@ -11235,7 +11273,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.11(@swc/core@1.10.4)(esbuild@0.24.2)(webpack@5.97.1) + terser-webpack-plugin: 5.3.11(@swc/core@1.10.4(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 2699ab38a890e..70ffb2e980f5c 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -16,7 +16,7 @@ import GraphContainer, { CommitDateTimeSources, refZone } from '@gitkraken/gitkr import type { SlChangeEvent } from '@shoelace-style/shoelace'; import SlOption from '@shoelace-style/shoelace/dist/react/option/index.js'; import SlSelect from '@shoelace-style/shoelace/dist/react/select/index.js'; -import type { FormEvent, MouseEvent, ReactElement } from 'react'; +import type { FormEvent, MouseEvent } from 'react'; import React, { createElement, useEffect, useMemo, useRef, useState } from 'react'; import { getPlatform } from '@env/platform'; import type { ConnectCloudIntegrationsCommandArgs } from '../../../../commands/cloudIntegrations'; @@ -147,7 +147,7 @@ const getGraphDateFormatter = (config?: GraphComponentConfig): OnFormatCommitDat formatCommitDateTime(commitDateTime, config?.dateStyle, config?.dateFormat, source); }; -const createIconElements = (): Record => { +const createIconElements = () => { const iconList = [ 'head', 'remote', @@ -187,7 +187,7 @@ const createIconElements = (): Record => { const miniIconList = ['upstream-ahead', 'upstream-behind']; - const elementLibrary: Record = {}; + const elementLibrary: Record = {}; iconList.forEach(iconKey => { elementLibrary[iconKey] = createElement('span', { className: `graph-icon icon--${iconKey}` }); }); @@ -205,6 +205,7 @@ const createIconElements = (): Record => { const iconElementLibrary = createIconElements(); const getIconElementLibrary = (iconKey: string) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return iconElementLibrary[iconKey]; }; diff --git a/src/webviews/apps/plus/graph/graph.scss b/src/webviews/apps/plus/graph/graph.scss index dc11f76773fd3..63f74cdc00798 100644 --- a/src/webviews/apps/plus/graph/graph.scss +++ b/src/webviews/apps/plus/graph/graph.scss @@ -1220,7 +1220,7 @@ gl-feature-gate gl-feature-badge { font-weight: normal !important; line-height: 19px !important; - &.in { + &.show { opacity: 1; } diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index cccda8ee1d245..4a51bf0498170 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -1,7 +1,7 @@ /*global document window*/ import type { CssVariables, GraphRef, GraphRefOptData, GraphRow } from '@gitkraken/gitkraken-components'; import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; +import { createRoot } from 'react-dom/client'; import type { GraphBranchesVisibility } from '../../../../config'; import type { SearchQuery } from '../../../../constants.search'; import type { GitGraphRowType } from '../../../../git/models/graph'; @@ -95,9 +95,10 @@ export class GraphApp extends App { this.ensureTheming(this.state); - const $root = document.getElementById('root'); - if ($root != null) { - render( + const $rootElement = document.getElementById('root'); + const root = createRoot($rootElement!); + if ($rootElement != null) { + root.render( { onSearchPromise={(...params) => this.onSearchPromise(...params)} onSearchOpenInView={(...params) => this.onSearchOpenInView(...params)} />, - $root, ); disposables.push({ - dispose: () => unmountComponentAtNode($root), + dispose: () => { + root.unmount(); + }, }); } From c32427f34d71e57e99cdcafa3455f49fa196efa0 Mon Sep 17 00:00:00 2001 From: nzaytsev Date: Thu, 30 Jan 2025 16:41:56 +0700 Subject: [PATCH 2/2] Makes the graph application web-components based --- ThirdPartyNotices.txt | 163 +- package.json | 3 +- pnpm-lock.yaml | 54 +- scripts/esbuild.mjs | 2 +- src/system/function.ts | 15 + src/webviews/apps/plus/graph/GraphWrapper.tsx | 1922 ----------------- .../plus/graph/actions/gitActionsButtons.tsx | 22 +- .../graph/actions/gitActionsButtons.wc.ts | 13 + src/webviews/apps/plus/graph/context.ts | 4 + src/webviews/apps/plus/graph/graph-app.ts | 165 ++ src/webviews/apps/plus/graph/graph-header.ts | 1160 ++++++++++ .../graph-wrapper/graph-wrapper.react.tsx | 570 +++++ .../plus/graph/graph-wrapper/graph-wrapper.ts | 316 +++ src/webviews/apps/plus/graph/graph.html | 7 +- src/webviews/apps/plus/graph/graph.scss | 23 +- src/webviews/apps/plus/graph/graph.ts | 289 +++ src/webviews/apps/plus/graph/graph.tsx | 697 ------ .../plus/graph/hover/graphHover.react.tsx | 5 - .../apps/plus/graph/hover/graphHover.ts | 3 +- .../graph/minimap/minimap-container.react.tsx | 12 - .../apps/plus/graph/minimap/minimap.react.tsx | 12 - .../apps/plus/graph/sidebar/sidebar.react.tsx | 5 - .../apps/plus/graph/sidebar/sidebar.ts | 29 +- src/webviews/apps/plus/graph/stateProvider.ts | 341 +++ .../plus/shared/components/account-chip.ts | 13 +- .../components/merge-rebase-status.react.ts | 5 - src/webviews/apps/shared/app.ts | 10 + .../apps/shared/components/button.react.tsx | 5 - .../components/checkbox/checkbox.react.ts | 9 - .../apps/shared/components/checkbox/index.ts | 1 - .../components/indicators/indicator.react.ts | 4 - .../components/integrations/connect.react.tsx | 5 - .../components/overlays/popover.react.tsx | 5 - .../apps/shared/components/radio/index.ts | 1 - .../shared/components/radio/radio.react.ts | 14 - .../shared/components/react/feature-badge.tsx | 5 - .../shared/components/react/feature-gate.tsx | 5 - .../components/react/issue-pull-request.tsx | 12 - .../apps/shared/components/search/react.tsx | 15 - .../shared/components/search/search-input.ts | 4 - .../apps/shared/components/signal-utils.ts | 46 +- src/webviews/plus/graph/graphWebview.ts | 150 +- src/webviews/plus/graph/graphWebviewUtils.ts | 147 ++ src/webviews/plus/graph/protocol.ts | 10 - webpack.config.mjs | 2 +- 45 files changed, 3339 insertions(+), 2961 deletions(-) delete mode 100644 src/webviews/apps/plus/graph/GraphWrapper.tsx create mode 100644 src/webviews/apps/plus/graph/actions/gitActionsButtons.wc.ts create mode 100644 src/webviews/apps/plus/graph/context.ts create mode 100644 src/webviews/apps/plus/graph/graph-app.ts create mode 100644 src/webviews/apps/plus/graph/graph-header.ts create mode 100644 src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.react.tsx create mode 100644 src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.ts create mode 100644 src/webviews/apps/plus/graph/graph.ts delete mode 100644 src/webviews/apps/plus/graph/graph.tsx delete mode 100644 src/webviews/apps/plus/graph/hover/graphHover.react.tsx delete mode 100644 src/webviews/apps/plus/graph/minimap/minimap-container.react.tsx delete mode 100644 src/webviews/apps/plus/graph/minimap/minimap.react.tsx delete mode 100644 src/webviews/apps/plus/graph/sidebar/sidebar.react.tsx create mode 100644 src/webviews/apps/plus/graph/stateProvider.ts delete mode 100644 src/webviews/apps/plus/shared/components/merge-rebase-status.react.ts delete mode 100644 src/webviews/apps/shared/components/button.react.tsx delete mode 100644 src/webviews/apps/shared/components/checkbox/checkbox.react.ts delete mode 100644 src/webviews/apps/shared/components/indicators/indicator.react.ts delete mode 100644 src/webviews/apps/shared/components/integrations/connect.react.tsx delete mode 100644 src/webviews/apps/shared/components/overlays/popover.react.tsx delete mode 100644 src/webviews/apps/shared/components/radio/radio.react.ts delete mode 100644 src/webviews/apps/shared/components/react/feature-badge.tsx delete mode 100644 src/webviews/apps/shared/components/react/feature-gate.tsx delete mode 100644 src/webviews/apps/shared/components/react/issue-pull-request.tsx delete mode 100644 src/webviews/apps/shared/components/search/react.tsx create mode 100644 src/webviews/plus/graph/graphWebviewUtils.ts diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 3a89fcc45732e..e4da4e0a9511b 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -17,22 +17,23 @@ This project incorporates components from the projects listed below. 12. @opentelemetry/resources version 1.30.1 (https://github.com/open-telemetry/opentelemetry-js) 13. @opentelemetry/sdk-trace-base version 1.30.1 (https://github.com/open-telemetry/opentelemetry-js) 14. @opentelemetry/semantic-conventions version 1.28.0 (https://github.com/open-telemetry/opentelemetry-js) -15. @shoelace-style/shoelace version 2.19.1 (https://github.com/shoelace-style/shoelace) -16. @vscode/codicons version 0.0.36 (https://github.com/microsoft/vscode-codicons) -17. billboard.js version 3.14.3 (https://github.com/naver/billboard.js) -18. https-proxy-agent version 5.0.1 (https://github.com/TooTallNate/node-https-proxy-agent) -19. iconv-lite version 0.6.3 (https://github.com/ashtuchkin/iconv-lite) -20. lit version 3.2.1 (https://github.com/lit/lit) -21. marked version 15.0.6 (https://github.com/markedjs/marked) -22. microsoft/vscode (https://github.com/microsoft/vscode) -23. node-fetch version 2.7.0 (https://github.com/bitinn/node-fetch) -24. os-browserify version 0.3.0 (https://github.com/CoderPuppy/os-browserify) -25. path-browserify version 1.0.1 (https://github.com/browserify/path-browserify) -26. react-dom version 19.0.0 (https://github.com/facebook/react) -27. react version 19.0.0 (https://github.com/facebook/react) -28. signal-utils version 0.21.1 (https://github.com/proposal-signals/signal-utils) -29. slug version 10.0.0 (https://github.com/Trott/slug) -30. sortablejs version 1.15.0 (https://github.com/SortableJS/Sortable) +15. @r2wc/react-to-web-component version 2.0.4 (https://github.com/bitovi/react-to-web-component) +16. @shoelace-style/shoelace version 2.19.1 (https://github.com/shoelace-style/shoelace) +17. @vscode/codicons version 0.0.36 (https://github.com/microsoft/vscode-codicons) +18. billboard.js version 3.14.3 (https://github.com/naver/billboard.js) +19. https-proxy-agent version 5.0.1 (https://github.com/TooTallNate/node-https-proxy-agent) +20. iconv-lite version 0.6.3 (https://github.com/ashtuchkin/iconv-lite) +21. lit version 3.2.1 (https://github.com/lit/lit) +22. marked version 15.0.6 (https://github.com/markedjs/marked) +23. microsoft/vscode (https://github.com/microsoft/vscode) +24. node-fetch version 2.7.0 (https://github.com/bitinn/node-fetch) +25. os-browserify version 0.3.0 (https://github.com/CoderPuppy/os-browserify) +26. path-browserify version 1.0.1 (https://github.com/browserify/path-browserify) +27. react-dom version 19.0.0 (https://github.com/facebook/react) +28. react version 19.0.0 (https://github.com/facebook/react) +29. signal-utils version 0.21.1 (https://github.com/proposal-signals/signal-utils) +30. slug version 10.0.0 (https://github.com/Trott/slug) +31. sortablejs version 1.15.0 (https://github.com/SortableJS/Sortable) %% @gk-nzaytsev/fast-string-truncated-width NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -1324,6 +1325,136 @@ END OF @opentelemetry/sdk-trace-base NOTICES AND INFORMATION ========================================= END OF @opentelemetry/semantic-conventions NOTICES AND INFORMATION +%% @r2wc/react-to-web-component NOTICES AND INFORMATION BEGIN HERE +========================================= +# React to Web Component + +`@r2wc/react-to-web-component` converts [React](https://reactjs.org/) components to [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)! It lets you share React components as native elements that **don't** require being mounted through React. The custom element acts as a wrapper for the underlying React component. Use these custom elements with any project that uses HTML even in any framework (vue, svelte, angular, ember, canjs) the same way you would use standard HTML elements. + +> Note: The latest version of this package only works with the React 18. If you are using React 16 or 17, please use version 1. + +`@r2wc/react-to-web-component`: + +- Works in all modern browsers. (Edge needs a [customElements polyfill](https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements)). +- Is `1.26KB` minified and gzipped. + +## Setup + +To install from npm: + +``` +npm install @r2wc/react-to-web-component +``` + +## Need help or have questions? + +This project is supported by [Bitovi, a React consultancy](https://www.bitovi.com/frontend-javascript-consulting/react-consulting). You can get help or ask questions on our: + +- [Discord Community](https://discord.gg/J7ejFsZnJ4) +- [Twitter](https://twitter.com/bitovi) + +Or, you can hire us for training, consulting, or development. [Set up a free consultation.](https://www.bitovi.com/frontend-javascript-consulting/react-consulting) + +## Basic Use + +For basic usage, we will use this simple React component: + +```js +const Greeting = () => { + return

Hello, World!

+} +``` + +With our React component complete, all we have to do is call `r2wc` and [customElements.define](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define) to create and define our custom element: + +```js +import r2wc from "@r2wc/react-to-web-component" + +const WebGreeting = r2wc(Greeting) + +customElements.define("web-greeting", WebGreeting) +``` + +Now we can use `` like any other HTML element! + +```html + +

Greeting Demo

+ + + +``` + +In the above case, the web-greeting custom element is not making use of the `name` property from our `Greeting` component. + +## Working with Attributes + +By default, custom elements created by `r2wc` only pass properties to the underlying React component. To make attributes work, you must specify your component's props. + +```js +const Greeting = ({ name }) => { + return

Hello, {name}!

+} + +const WebGreeting = r2wc(Greeting, { + props: { + name: "string", + }, +}) +``` + +Now `r2wc` will know to look for `name` attributes +as follows: + +```html + +

Greeting Demo

+ + + +``` + +For projects needing more advanced usage of the web components, see our [programatic usage and declarative demos](docs/programatic-usage.md). + +We also have a [complete example using a third party library](docs/complete-example.md). + +## Examples + +- [Hello World](https://codesandbox.io/s/hello-world-md5oih) - The quintessential software demo! +- [All the Props](https://codesandbox.io/s/all-the-props-n8z5hv) - A demo of all the prop transform types that R2WC supports. +- [Header Example](https://codesandbox.io/s/example-header-blog-7k313l) - An example reusable Header component. +- [MUI Button](https://codesandbox.io/s/example-mui-button-qwidh9) - An example application using an MUI button with theme customization. +- [Checklist Demo](https://codesandbox.io/s/example-checklist-blog-y3nqwx) - An example Checklist application. + +## Blog Posts + +R2WC with Vite [View Post](https://www.bitovi.com/blog/react-everywhere-with-vite-and-react-to-webcomponent) + +R2WC with Create React App (CRA) [View Post](https://www.bitovi.com/blog/how-to-create-a-web-component-with-create-react-app) + +## How it works + +Check out our [full API documentation](https://github.com/bitovi/react-to-web-component/blob/main/docs/api.md). + +Under the hood, `r2wc` creates a `CustomElementConstructor` with custom getters/setters and life cycle methods that keep track of the props that you have defined. When a property is set, its custom setter: + +- re-renders the React component inside the custom element. +- creates an enumerable getter / setter on the instance to save the set value and avoid hitting the proxy in the future. + +Also: + +- Enumerable properties and values on the custom element are used as the `props` passed to the React component. +- The React component is not rendered until the custom element is inserted into the page. + +# We want to hear from you. + +Come chat with us about open source in our Bitovi community [Discord](https://discord.gg/J7ejFsZnJ4). + +See what we're up to by following us on [Twitter](https://twitter.com/bitovi). + +========================================= +END OF @r2wc/react-to-web-component NOTICES AND INFORMATION + %% @shoelace-style/shoelace NOTICES AND INFORMATION BEGIN HERE ========================================= Copyright (c) 2020 A Beautiful Site, LLC diff --git a/package.json b/package.json index a31d7448ebc4a..81bf0b5e9923d 100644 --- a/package.json +++ b/package.json @@ -20116,7 +20116,7 @@ "vscode:prepublish": "pnpm run bundle" }, "dependencies": { - "@gitkraken/gitkraken-components": "11.0.0-vnext", + "@gitkraken/gitkraken-components": "11.0.0-vnext.3", "@gitkraken/provider-apis": "0.26.1", "@gitkraken/shared-web-components": "0.1.1-rc.15", "@gk-nzaytsev/fast-string-truncated-width": "1.1.0", @@ -20133,6 +20133,7 @@ "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0", + "@r2wc/react-to-web-component": "^2.0.4", "@shoelace-style/shoelace": "2.19.1", "@vscode/codicons": "0.0.36", "billboard.js": "3.14.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee900bc397426..4e83fe45ae5c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: .: dependencies: '@gitkraken/gitkraken-components': - specifier: 11.0.0-vnext - version: 11.0.0-vnext(@types/react@19.0.6)(react@19.0.0) + specifier: 11.0.0-vnext.3 + version: 11.0.0-vnext.3(@types/react@19.0.6)(react@19.0.0) '@gitkraken/provider-apis': specifier: 0.26.1 version: 0.26.1(encoding@0.1.13) @@ -65,6 +65,9 @@ importers: '@opentelemetry/semantic-conventions': specifier: 1.28.0 version: 1.28.0 + '@r2wc/react-to-web-component': + specifier: ^2.0.4 + version: 2.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@shoelace-style/shoelace': specifier: 2.19.1 version: 2.19.1(@floating-ui/utils@0.2.9)(@types/react@19.0.6) @@ -368,10 +371,6 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.26.0': - resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.26.7': resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==} engines: {node: '>=6.9.0'} @@ -606,8 +605,8 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - '@gitkraken/gitkraken-components@11.0.0-vnext': - resolution: {integrity: sha512-xxSzjnAQLNL2Lax3mEuODEXALYAKH7R2R/F/RClHrlNeDNEf9CWJvy2JwFEYwylPQoKUwEi+k3kpa1M3h6EoTA==} + '@gitkraken/gitkraken-components@11.0.0-vnext.3': + resolution: {integrity: sha512-tS/R3lewd7qqKm4L3v/2SY9++aZExINC3L5wN75ljY3z0Cei9uVKi7EIVeIR/Zil/apwnti7SfYY9+yS+9XzQg==} peerDependencies: react: 19.0.0 @@ -1107,6 +1106,15 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@r2wc/core@1.2.0': + resolution: {integrity: sha512-vAfiuS5KywtV54SRzc4maEHcpdgeUyJzln+ATpNCOkO+ArIuOkTXd92b5YauVAd0A8B2rV/y9OeVW19vb73bUQ==} + + '@r2wc/react-to-web-component@2.0.4': + resolution: {integrity: sha512-g1dtTTEGETNUimYldTW+2hxY3mmJZjzPEca0vqCutUht2GHmpK9mT5r/urmEI7uSbOkn6HaymosgVy26lvU1JQ==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + '@react-aria/ssr@3.9.7': resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==} engines: {node: '>= 12'} @@ -1123,8 +1131,8 @@ packages: peerDependencies: react: '>=16.8.0' - '@restart/ui@1.9.2': - resolution: {integrity: sha512-MWWqJqSyqUWWPBOOiRQrX57CBc/9CoYONg7sE+uag72GCAuYrHGU5c49vU5s4BUSBgiKNY6rL7TULqGDrouUaA==} + '@restart/ui@1.9.4': + resolution: {integrity: sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==} peerDependencies: react: '>=16.14.0' react-dom: '>=16.14.0' @@ -5700,10 +5708,6 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': {} - '@babel/runtime@7.26.0': - dependencies: - regenerator-runtime: 0.14.1 - '@babel/runtime@7.26.7': dependencies: regenerator-runtime: 0.14.1 @@ -5878,7 +5882,7 @@ snapshots: '@gar/promisify@1.1.3': {} - '@gitkraken/gitkraken-components@11.0.0-vnext(@types/react@19.0.6)(react@19.0.0)': + '@gitkraken/gitkraken-components@11.0.0-vnext.3(@types/react@19.0.6)(react@19.0.0)': dependencies: '@axosoft/react-virtualized': 9.22.3-gitkraken.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) classnames: 2.5.1 @@ -6333,6 +6337,14 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@r2wc/core@1.2.0': {} + + '@r2wc/react-to-web-component@2.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@r2wc/core': 1.2.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@react-aria/ssr@3.9.7(react@19.0.0)': dependencies: '@swc/helpers': 0.5.15 @@ -6348,9 +6360,9 @@ snapshots: dequal: 2.0.3 react: 19.0.0 - '@restart/ui@1.9.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@restart/ui@1.9.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@babel/runtime': 7.26.0 + '@babel/runtime': 7.26.7 '@popperjs/core': 2.11.8 '@react-aria/ssr': 3.9.7(react@19.0.0) '@restart/hooks': 0.5.1(react@19.0.0) @@ -10183,9 +10195,9 @@ snapshots: react-bootstrap@2.10.7(@types/react@19.0.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@babel/runtime': 7.26.0 + '@babel/runtime': 7.26.7 '@restart/hooks': 0.4.16(react@19.0.0) - '@restart/ui': 1.9.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@restart/ui': 1.9.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/prop-types': 15.7.14 '@types/react-transition-group': 4.4.12(@types/react@19.0.6) classnames: 2.5.1 @@ -10222,7 +10234,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@babel/runtime': 7.26.0 + '@babel/runtime': 7.26.7 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -11123,7 +11135,7 @@ snapshots: uncontrollable@7.2.1(react@19.0.0): dependencies: - '@babel/runtime': 7.26.0 + '@babel/runtime': 7.26.7 '@types/react': 19.0.6 invariant: 2.2.4 react: 19.0.0 diff --git a/scripts/esbuild.mjs b/scripts/esbuild.mjs index 10b1e111c3107..ffceca3259420 100644 --- a/scripts/esbuild.mjs +++ b/scripts/esbuild.mjs @@ -140,7 +140,7 @@ async function buildGraphWebview(mode) { const result = await esbuild.build({ bundle: true, - entryPoints: ['src/webviews/apps/plus/graph/graph.tsx'], + entryPoints: ['src/webviews/apps/plus/graph/graph.ts'], entryNames: '[dir]/graph', alias: { '@env': path.resolve(__dirname, 'src', 'env', 'browser'), diff --git a/src/system/function.ts b/src/system/function.ts index b611634be5b93..3a9deee6c096d 100644 --- a/src/system/function.ts +++ b/src/system/function.ts @@ -110,6 +110,21 @@ export function debounce ReturnType>( return debounced; } +export function debounced ReturnType>(delay: number) { + return (_target: any, _fieldName: string, targetFields: { value?: T }): any => { + console.log('debounced', targetFields, _fieldName); + if (!targetFields.value) { + throw new Error('@debounced can only be used on methods'); + } + const debounced = debounce(targetFields.value, delay); + return { + ...targetFields, + // @ts-expect-error Deferrable to T is safe + value: debounced as T, + }; + }; +} + const comma = ','; const equals = '='; const openBrace = '{'; diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx deleted file mode 100644 index 70ffb2e980f5c..0000000000000 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ /dev/null @@ -1,1922 +0,0 @@ -import type { - CommitType, - GraphColumnMode, - GraphColumnSetting, - GraphColumnsSettings, - GraphContainerProps, - GraphPlatform, - GraphRef, - GraphRefGroup, - GraphRefOptData, - GraphRow, - GraphZoneType, - OnFormatCommitDateTime, -} from '@gitkraken/gitkraken-components'; -import GraphContainer, { CommitDateTimeSources, refZone } from '@gitkraken/gitkraken-components'; -import type { SlChangeEvent } from '@shoelace-style/shoelace'; -import SlOption from '@shoelace-style/shoelace/dist/react/option/index.js'; -import SlSelect from '@shoelace-style/shoelace/dist/react/select/index.js'; -import type { FormEvent, MouseEvent } from 'react'; -import React, { createElement, useEffect, useMemo, useRef, useState } from 'react'; -import { getPlatform } from '@env/platform'; -import type { ConnectCloudIntegrationsCommandArgs } from '../../../../commands/cloudIntegrations'; -import type { BranchGitCommandArgs } from '../../../../commands/git/branch'; -import type { DateStyle, GraphBranchesVisibility } from '../../../../config'; -import { GlCommand } from '../../../../constants.commands'; -import type { SearchQuery } from '../../../../constants.search'; -import type { Subscription } from '../../../../plus/gk/models/subscription'; -import { isSubscriptionPaid } from '../../../../plus/gk/utils/subscription.utils'; -import type { LaunchpadCommandArgs } from '../../../../plus/launchpad/launchpad'; -import { createCommandLink } from '../../../../system/commands'; -import { filterMap, first, groupByFilterMap, join } from '../../../../system/iterable'; -import { createWebviewCommandLink } from '../../../../system/webview'; -import type { - DidEnsureRowParams, - DidGetRowHoverParams, - DidSearchParams, - GraphAvatars, - GraphColumnName, - GraphColumnsConfig, - GraphComponentConfig, - GraphExcludedRef, - GraphExcludeTypes, - GraphItemContext, - GraphMinimapMarkerTypes, - GraphMissingRefsMetadata, - GraphRefMetadataItem, - GraphRepository, - GraphSearchMode, - GraphSearchResults, - GraphSearchResultsError, - InternalNotificationType, - State, - UpdateGraphConfigurationParams, - UpdateStateCallback, -} from '../../../plus/graph/protocol'; -import { - DidChangeAvatarsNotification, - DidChangeBranchStateNotification, - DidChangeColumnsNotification, - DidChangeGraphConfigurationNotification, - DidChangeRefsMetadataNotification, - DidChangeRefsVisibilityNotification, - DidChangeRepoConnectionNotification, - DidChangeRowsNotification, - DidChangeRowsStatsNotification, - DidChangeSelectionNotification, - DidChangeSubscriptionNotification, - DidChangeWorkingTreeNotification, - DidFetchNotification, - DidSearchNotification, - DidStartFeaturePreviewNotification, -} from '../../../plus/graph/protocol'; -import type { IpcNotification } from '../../../protocol'; -import { DidChangeHostWindowFocusNotification } from '../../../protocol'; -import { GlButton } from '../../shared/components/button.react'; -import { GlCheckbox } from '../../shared/components/checkbox'; -import { CodeIcon } from '../../shared/components/code-icon.react'; -import { GlIndicator } from '../../shared/components/indicators/indicator.react'; -import { GlMarkdown } from '../../shared/components/markdown/markdown.react'; -import { MenuDivider, MenuItem, MenuLabel } from '../../shared/components/menu/react'; -import { GlPopover } from '../../shared/components/overlays/popover.react'; -import { GlTooltip } from '../../shared/components/overlays/tooltip.react'; -import type { RadioGroup } from '../../shared/components/radio/radio-group'; -import { GlRadio, GlRadioGroup } from '../../shared/components/radio/radio.react'; -import { GlFeatureBadge } from '../../shared/components/react/feature-badge'; -import { GlFeatureGate } from '../../shared/components/react/feature-gate'; -import { GlIssuePullRequest } from '../../shared/components/react/issue-pull-request'; -import { GlSearchBox } from '../../shared/components/search/react'; -import type { - SearchModeChangeEventDetail, - SearchNavigationEventDetail, -} from '../../shared/components/search/search-box'; -import type { DateTimeFormat } from '../../shared/date'; -import { formatDate, fromNow } from '../../shared/date'; -import { emitTelemetrySentEvent } from '../../shared/telemetry'; -import { GlMergeConflictWarning } from '../shared/components/merge-rebase-status.react'; -import { GitActionsButtons } from './actions/gitActionsButtons'; -import { GlGraphHover } from './hover/graphHover.react'; -import type { GraphMinimapDaySelectedEventDetail } from './minimap/minimap'; -import { GlGraphMinimapContainer } from './minimap/minimap-container.react'; -import { GlGraphSideBar } from './sidebar/sidebar.react'; - -function getRemoteIcon(type: string | number) { - switch (type) { - case 'head': - return 'vm'; - case 'remote': - return 'cloud'; - case 'tag': - return 'tag'; - default: - return ''; - } -} - -export interface GraphWrapperProps { - nonce?: string; - state: State; - subscriber: (callback: UpdateStateCallback) => () => void; - onChangeColumns?: (colsSettings: GraphColumnsConfig) => void; - onChangeExcludeTypes?: (key: keyof GraphExcludeTypes, value: boolean) => void; - onChangeGraphConfiguration?: (changes: UpdateGraphConfigurationParams['changes']) => void; - onChangeGraphSearchMode?: (searchMode: GraphSearchMode) => void; - onChangeRefIncludes?: (branchesVisibility: GraphBranchesVisibility, refs?: GraphRefOptData[]) => void; - onChangeRefsVisibility?: (refs: GraphExcludedRef[], visible: boolean) => void; - onChangeSelection?: (rows: GraphRow[]) => void; - onChooseRepository?: () => void; - onDoubleClickRef?: (ref: GraphRef, metadata?: GraphRefMetadataItem) => void; - onDoubleClickRow?: (row: GraphRow, preserveFocus?: boolean) => void; - onEnsureRowPromise?: (id: string, select: boolean) => Promise; - onHoverRowPromise?: (row: GraphRow) => Promise; - onJumpToRefPromise?: (alt: boolean) => Promise<{ name: string; sha: string } | undefined>; - onMissingAvatars?: (emails: Record) => void; - onMissingRefsMetadata?: (metadata: GraphMissingRefsMetadata) => void; - onMoreRows?: (id?: string) => void; - onOpenPullRequest?: (pr: NonNullable['pr']>) => void; - onSearch?: (search: SearchQuery | undefined, options?: { limit?: number }) => void; - onSearchPromise?: ( - search: SearchQuery, - options?: { limit?: number; more?: boolean }, - ) => Promise; - onSearchOpenInView?: (search: SearchQuery) => void; -} - -const getGraphDateFormatter = (config?: GraphComponentConfig): OnFormatCommitDateTime => { - return (commitDateTime: number, source?: CommitDateTimeSources) => - formatCommitDateTime(commitDateTime, config?.dateStyle, config?.dateFormat, source); -}; - -const createIconElements = () => { - const iconList = [ - 'head', - 'remote', - 'remote-github', - 'remote-githubEnterprise', - 'remote-gitlab', - 'remote-gitlabSelfHosted', - 'remote-bitbucket', - 'remote-bitbucketServer', - 'remote-azureDevops', - 'tag', - 'stash', - 'check', - 'loading', - 'warning', - 'added', - 'modified', - 'deleted', - 'renamed', - 'resolved', - 'pull-request', - 'show', - 'hide', - 'branch', - 'graph', - 'commit', - 'author', - 'datetime', - 'message', - 'changes', - 'files', - 'worktree', - 'issue-github', - 'issue-gitlab', - 'issue-jiraCloud', - ]; - - const miniIconList = ['upstream-ahead', 'upstream-behind']; - - const elementLibrary: Record = {}; - iconList.forEach(iconKey => { - elementLibrary[iconKey] = createElement('span', { className: `graph-icon icon--${iconKey}` }); - }); - miniIconList.forEach(iconKey => { - elementLibrary[iconKey] = createElement('span', { className: `graph-icon mini-icon icon--${iconKey}` }); - }); - //TODO: fix this once the styling is properly configured component-side - elementLibrary.settings = createElement('span', { - className: 'graph-icon icon--settings', - style: { fontSize: '1.1rem', right: '0px', top: '-1px' }, - }); - return elementLibrary; -}; - -const iconElementLibrary = createIconElements(); - -const getIconElementLibrary = (iconKey: string) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return iconElementLibrary[iconKey]; -}; - -const getClientPlatform = (): GraphPlatform => { - switch (getPlatform()) { - case 'web-macOS': - return 'darwin'; - case 'web-windows': - return 'win32'; - case 'web-linux': - default: - return 'linux'; - } -}; - -const clientPlatform = getClientPlatform(); - -interface SelectionContext { - listDoubleSelection?: boolean; - listMultiSelection?: boolean; - webviewItems?: string; - webviewItemsValues?: GraphItemContext[]; -} - -interface SelectionContexts { - contexts: Map; - selectedShas: Set; -} - -const emptySelectionContext: SelectionContext = { - listDoubleSelection: false, - listMultiSelection: false, - webviewItems: undefined, - webviewItemsValues: undefined, -}; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function GraphWrapper({ - subscriber, - nonce, - state, - onChooseRepository, - onChangeColumns, - onChangeExcludeTypes, - onChangeGraphConfiguration, - onChangeGraphSearchMode, - onChangeRefIncludes, - onChangeRefsVisibility, - onChangeSelection, - onDoubleClickRef, - onDoubleClickRow, - onEnsureRowPromise, - onHoverRowPromise, - onJumpToRefPromise, - onMissingAvatars, - onMissingRefsMetadata, - onMoreRows, - onOpenPullRequest, - onSearch, - onSearchPromise, - onSearchOpenInView, -}: GraphWrapperProps): React.JSX.Element { - const graphRef = useRef(null); - - const [rows, setRows] = useState(state.rows ?? []); - const [rowsStats, setRowsStats] = useState(state.rowsStats); - const [rowsStatsLoading, setRowsStatsLoading] = useState(state.rowsStatsLoading); - const [avatars, setAvatars] = useState(state.avatars); - const [downstreams, setDownstreams] = useState(state.downstreams ?? {}); - const [refsMetadata, setRefsMetadata] = useState(state.refsMetadata); - const [repos, setRepos] = useState(state.repositories ?? []); - const [repo, setRepo] = useState( - repos.find(item => item.path === state.selectedRepository), - ); - const [branchesVisibility, setBranchesVisibility] = useState(state.branchesVisibility); - const [branchState, setBranchState] = useState(state.branchState); - const [selectedRows, setSelectedRows] = useState(state.selectedRows); - const [activeRow, setActiveRow] = useState(state.activeRow); - const [activeDay, setActiveDay] = useState(state.activeDay); - const [selectionContexts, setSelectionContexts] = useState(); - const [visibleDays, setVisibleDays] = useState(state.visibleDays); - const [graphConfig, setGraphConfig] = useState(state.config); - // const [graphDateFormatter, setGraphDateFormatter] = useState(getGraphDateFormatter(config)); - const [columns, setColumns] = useState(state.columns); - const [excludeRefsById, setExcludeRefsById] = useState(state.excludeRefs); - const [excludeTypes, setExcludeTypes] = useState(state.excludeTypes); - const [includeOnlyRefsById, setIncludeOnlyRefsById] = useState(state.includeOnlyRefs); - const [context, setContext] = useState(state.context); - const [pagingHasMore, setPagingHasMore] = useState(state.paging?.hasMore ?? false); - const [isLoading, setIsLoading] = useState(state.loading); - const [styleProps, setStyleProps] = useState(state.theming); - const [branch, setBranch] = useState(state.branch); - const [lastFetched, setLastFetched] = useState(state.lastFetched); - const [windowFocused, setWindowFocused] = useState(state.windowFocused); - const [allowed, setAllowed] = useState(state.allowed ?? false); - const [subscription, setSubscription] = useState(state.subscription); - const [featurePreview, setFeaturePreview] = useState(state.featurePreview); - - // search state - const searchEl = useRef(null); - const [searchQuery, setSearchQuery] = useState(undefined); - const { results, resultsError } = getSearchResultModel(state); - const [searchResults, setSearchResults] = useState(results); - const [searchResultsError, setSearchResultsError] = useState(resultsError); - const [searchResultsHidden, setSearchResultsHidden] = useState(false); - const [searching, setSearching] = useState(false); - - // working tree state - const [workingTreeStats, setWorkingTreeStats] = useState( - state.workingTreeStats ?? { added: 0, modified: 0, deleted: 0 }, - ); - const branchName = branch?.name; - - const minimap = useRef(undefined); - const hover = useRef(undefined); - - const ensuredIds = useRef>(new Set()); - const ensuredSkippedIds = useRef>(new Set()); - - function updateState( - state: State, - type?: IpcNotification | InternalNotificationType, - themingChanged?: boolean, - ) { - if (themingChanged) { - setStyleProps(state.theming); - } - - switch (type) { - case 'didChangeTheme': - if (!themingChanged) { - setStyleProps(state.theming); - } - break; - case DidStartFeaturePreviewNotification: - setFeaturePreview(state.featurePreview); - setAllowed(state.allowed ?? false); - break; - case DidChangeAvatarsNotification: - setAvatars(state.avatars); - break; - case DidChangeBranchStateNotification: - setBranchState(state.branchState); - break; - case DidChangeHostWindowFocusNotification: - setWindowFocused(state.windowFocused); - break; - case DidChangeRefsMetadataNotification: - setRefsMetadata(state.refsMetadata); - break; - case DidChangeColumnsNotification: - setColumns(state.columns); - setContext(state.context); - break; - case DidChangeRowsNotification: - hover.current?.reset(); - setRows(state.rows ?? []); - setRowsStats(state.rowsStats); - setRowsStatsLoading(state.rowsStatsLoading); - setSelectedRows(state.selectedRows); - setAvatars(state.avatars); - setDownstreams(state.downstreams ?? {}); - setRefsMetadata(state.refsMetadata); - setPagingHasMore(state.paging?.hasMore ?? false); - setIsLoading(state.loading); - break; - case DidChangeRowsStatsNotification: - hover.current?.reset(); - setRowsStats(state.rowsStats); - setRowsStatsLoading(state.rowsStatsLoading); - break; - case DidSearchNotification: { - const { results, resultsError } = getSearchResultModel(state); - setSearchResultsError(resultsError); - setSearchResults(results); - setSelectedRows(state.selectedRows); - setSearching(false); - break; - } - case DidChangeGraphConfigurationNotification: - setGraphConfig(state.config); - break; - case DidChangeSelectionNotification: - setSelectedRows(state.selectedRows); - break; - case DidChangeRefsVisibilityNotification: - setBranchesVisibility(state.branchesVisibility); - setExcludeRefsById(state.excludeRefs); - setExcludeTypes(state.excludeTypes); - setIncludeOnlyRefsById(state.includeOnlyRefs); - // Hack to force the Graph to maintain the selected rows - if (state.selectedRows != null) { - const shas = Object.keys(state.selectedRows); - if (shas.length) { - queueMicrotask(() => graphRef?.current?.selectCommits(shas, false, true)); - } - } - break; - case DidChangeSubscriptionNotification: - setAllowed(state.allowed ?? false); - setSubscription(state.subscription); - break; - case DidChangeWorkingTreeNotification: - setWorkingTreeStats(state.workingTreeStats ?? { added: 0, modified: 0, deleted: 0 }); - break; - case DidFetchNotification: - setLastFetched(state.lastFetched); - break; - case DidChangeRepoConnectionNotification: - setRepos(state.repositories ?? []); - setRepo(state.repositories?.find(item => item.path === state.selectedRepository)); - break; - default: { - hover.current?.reset(); - setAllowed(state.allowed ?? false); - if (!themingChanged) { - setStyleProps(state.theming); - } - setBranch(state.branch); - setLastFetched(state.lastFetched); - setColumns(state.columns); - setRows(state.rows ?? []); - setRowsStats(state.rowsStats); - setRowsStatsLoading(state.rowsStatsLoading); - setWorkingTreeStats(state.workingTreeStats ?? { added: 0, modified: 0, deleted: 0 }); - setGraphConfig(state.config); - setSelectedRows(state.selectedRows); - setExcludeRefsById(state.excludeRefs); - setExcludeTypes(state.excludeTypes); - setIncludeOnlyRefsById(state.includeOnlyRefs); - setContext(state.context); - setAvatars(state.avatars ?? {}); - setDownstreams(state.downstreams ?? {}); - setBranchesVisibility(state.branchesVisibility); - setBranchState(state.branchState); - setRefsMetadata(state.refsMetadata); - setPagingHasMore(state.paging?.hasMore ?? false); - setRepos(state.repositories ?? []); - setRepo(repos.find(item => item.path === state.selectedRepository)); - // setGraphDateFormatter(getGraphDateFormatter(config)); - setSubscription(state.subscription); - setFeaturePreview(state.featurePreview); - - const { results, resultsError } = getSearchResultModel(state); - setSearchResultsError(resultsError); - setSearchResults(results); - - setIsLoading(state.loading); - break; - } - } - } - - useEffect(() => subscriber?.(updateState), []); - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - const sha = getActiveRowInfo(activeRow ?? state.activeRow)?.id; - if (sha == null) return; - - // TODO@eamodio a bit of a hack since the graph container ref isn't exposed in the types - const graph = (graphRef.current as any)?.graphContainerRef.current; - if (!e.composedPath().some(el => el === graph)) return; - - const row = rows.find(r => r.sha === sha); - if (row == null) return; - - onDoubleClickRow?.(row, e.key !== 'Enter'); - } - }; - - useEffect(() => { - window.addEventListener('keydown', handleKeyDown); - - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [activeRow]); - - const handleOnMinimapDaySelected = (e: CustomEvent) => { - let { sha } = e.detail; - if (sha == null) { - const date = e.detail.date?.getTime(); - if (date == null) return; - - // Find closest row to the date - const closest = rows.reduce((prev, curr) => - Math.abs(curr.date - date) < Math.abs(prev.date - date) ? curr : prev, - ); - sha = closest.sha; - } - - graphRef.current?.selectCommits([sha], false, true); - - queueMicrotask( - () => - e.target && - emitTelemetrySentEvent<'graph/minimap/day/selected'>(e.target, { - name: 'graph/minimap/day/selected', - data: {}, - }), - ); - }; - - const handleOnMinimapToggle = (_e: React.MouseEvent) => { - onChangeGraphConfiguration?.({ minimap: !graphConfig?.minimap }); - }; - - // This can only be applied to one radio button for now due to a bug in the component: https://github.com/microsoft/fast/issues/6381 - const handleOnMinimapDataTypeChange = (e: Event | FormEvent) => { - if (graphConfig == null) return; - - const $el = e.target as RadioGroup; - const minimapDataType = $el.value === 'lines' ? 'lines' : 'commits'; - if (graphConfig.minimapDataType === minimapDataType) return; - - setGraphConfig({ ...graphConfig, minimapDataType: minimapDataType }); - onChangeGraphConfiguration?.({ minimapDataType: minimapDataType }); - }; - - const handleOnMinimapAdditionalTypesChange = (e: Event | FormEvent) => { - if (graphConfig?.minimapMarkerTypes == null) return; - - const $el = e.target as HTMLInputElement; - const value = $el.value as GraphMinimapMarkerTypes; - - if ($el.checked) { - if (!graphConfig.minimapMarkerTypes.includes(value)) { - const minimapMarkerTypes = [...graphConfig.minimapMarkerTypes, value]; - setGraphConfig({ ...graphConfig, minimapMarkerTypes: minimapMarkerTypes }); - onChangeGraphConfiguration?.({ minimapMarkerTypes: minimapMarkerTypes }); - } - } else { - const index = graphConfig.minimapMarkerTypes.indexOf(value); - if (index !== -1) { - const minimapMarkerTypes = [...graphConfig.minimapMarkerTypes]; - minimapMarkerTypes.splice(index, 1); - setGraphConfig({ ...graphConfig, minimapMarkerTypes: minimapMarkerTypes }); - onChangeGraphConfiguration?.({ minimapMarkerTypes: minimapMarkerTypes }); - } - } - }; - - const stopColumnResize = () => { - const activeResizeElement = document.querySelector('.graph-header .resizable.resizing'); - if (!activeResizeElement) return; - - // Trigger a mouseup event to reset the column resize state - document.dispatchEvent( - new MouseEvent('mouseup', { - view: window, - bubbles: true, - cancelable: true, - }), - ); - }; - - const handleOnGraphMouseLeave = (_event: React.MouseEvent) => { - minimap.current?.unselect(undefined, true); - stopColumnResize(); - }; - - const handleOnGraphRowHovered = ( - event: React.MouseEvent, - graphZoneType: GraphZoneType, - graphRow: GraphRow, - ) => { - if (graphZoneType === refZone) return; - - minimap.current?.select(graphRow.date, true); - - if (onHoverRowPromise == null) return; - - const hoverComponent = hover.current; - if (hoverComponent == null) return; - - const { clientX } = event; - - const rect = event.currentTarget.getBoundingClientRect() as DOMRect; - const x = clientX; - const y = rect.top; - const height = rect.height; - const width = 60; // Add some width, so `skidding` will be able to apply - - const anchor = { - getBoundingClientRect: function () { - return { - width: width, - height: height, - x: x, - y: y, - top: y, - left: x, - right: x + width, - bottom: y + height, - }; - }, - }; - - hoverComponent.requestMarkdown ??= onHoverRowPromise; - hoverComponent.onRowHovered(graphRow, anchor); - }; - - const handleOnGraphRowUnhovered = ( - event: React.MouseEvent, - graphZoneType: GraphZoneType, - graphRow: GraphRow, - ) => { - if (graphZoneType === refZone) return; - - hover.current?.onRowUnhovered(graphRow, event.relatedTarget); - }; - - useEffect(() => { - if (searchResultsError != null || searchResults == null || searchResults.count === 0 || searchQuery == null) { - return; - } - - searchEl.current?.logSearch(searchQuery); - }, [searchResults]); - - const searchPosition: number = useMemo(() => { - if (searchResults?.ids == null || !searchQuery?.query) return 0; - - const id = getActiveRowInfo(activeRow)?.id; - let searchIndex = id ? searchResults.ids[id]?.i : undefined; - if (searchIndex == null) { - [searchIndex] = getClosestSearchResultIndex(searchResults, searchQuery, activeRow); - } - return searchIndex < 1 ? 1 : searchIndex + 1; - }, [activeRow, searchResults]); - - const hasFilters = useMemo(() => { - if (graphConfig?.onlyFollowFirstParent) return true; - if (excludeTypes == null) return false; - - return Object.values(excludeTypes).includes(true); - }, [excludeTypes, graphConfig?.onlyFollowFirstParent]); - - const handleSearchInput = (e: CustomEvent) => { - const detail = e.detail; - setSearchQuery(detail); - - const isValid = detail.query.length >= 3; - setSearchResults(undefined); - setSearchResultsError(undefined); - setSearchResultsHidden(false); - setSearching(isValid); - onSearch?.(isValid ? detail : undefined); - }; - - const handleSearchOpenInView = () => { - if (searchQuery == null) return; - - onSearchOpenInView?.(searchQuery); - }; - - const handleSearchModeChange = (e: CustomEvent) => { - const { searchMode } = e.detail; - onChangeGraphSearchMode?.(searchMode); - }; - - const ensureSearchResultRow = async (id: string): Promise => { - if (onEnsureRowPromise == null) return id; - if (ensuredIds.current.has(id)) return id; - if (ensuredSkippedIds.current.has(id)) return undefined; - - let timeout: ReturnType | undefined = setTimeout(() => { - timeout = undefined; - setIsLoading(true); - }, 500); - - const e = await onEnsureRowPromise(id, false); - if (timeout == null) { - setIsLoading(false); - } else { - clearTimeout(timeout); - } - - if (e?.id === id) { - ensuredIds.current.add(id); - return id; - } - - if (e != null) { - ensuredSkippedIds.current.add(id); - } - return undefined; - }; - - const handleSearchNavigation = async (e: CustomEvent) => { - if (searchResults == null) return; - - const direction = e.detail?.direction ?? 'next'; - - let results = searchResults; - let count = results.count; - - let searchIndex; - let id: string | undefined; - - let next; - if (direction === 'first') { - next = false; - searchIndex = 0; - } else if (direction === 'last') { - next = false; - searchIndex = -1; - } else { - next = direction === 'next'; - [searchIndex, id] = getClosestSearchResultIndex(results, searchQuery, activeRow, next); - } - - let iterations = 0; - // Avoid infinite loops - while (iterations < 1000) { - iterations++; - - // Indicates a boundary and we need to load more results - if (searchIndex === -1) { - if (next) { - if (searchQuery != null && results?.paging?.hasMore) { - setSearching(true); - let moreResults; - try { - moreResults = await onSearchPromise?.(searchQuery, { more: true }); - } finally { - setSearching(false); - } - if (moreResults?.results != null && !('error' in moreResults.results)) { - if (count < moreResults.results.count) { - results = moreResults.results; - searchIndex = count; - count = results.count; - } else { - searchIndex = 0; - } - } else { - searchIndex = 0; - } - } else { - searchIndex = 0; - } - } else if (direction === 'last' && searchQuery != null && results?.paging?.hasMore) { - setSearching(true); - let moreResults; - try { - moreResults = await onSearchPromise?.(searchQuery, { limit: 0, more: true }); - } finally { - setSearching(false); - } - if (moreResults?.results != null && !('error' in moreResults.results)) { - if (count < moreResults.results.count) { - results = moreResults.results; - count = results.count; - } - searchIndex = count; - } - } else { - searchIndex = count - 1; - } - } - - id = id ?? getSearchResultIdByIndex(results, searchIndex); - if (id != null) { - id = await ensureSearchResultRow(id); - if (id != null) break; - } - - setSearchResultsHidden(true); - - searchIndex = getNextOrPreviousSearchResultIndex(searchIndex, next, results, searchQuery); - } - - if (id != null) { - queueMicrotask(() => graphRef.current?.selectCommits([id], false, true)); - } - }; - - const handleChooseRepository = () => { - onChooseRepository?.(); - }; - - const handleJumpToRef = async (e: MouseEvent) => { - const ref = await onJumpToRefPromise?.(e.altKey); - if (ref != null) { - const sha = await ensureSearchResultRow(ref.sha); - if (sha == null) return; - - queueMicrotask(() => graphRef.current?.selectCommits([sha], false, true)); - } - }; - - const handleFilterChange = (e: Event | FormEvent) => { - const $el = e.target as HTMLInputElement; - if ($el == null) return; - - const { checked } = $el; - - switch ($el.value) { - case 'mergeCommits': - onChangeGraphConfiguration?.({ dimMergeCommits: checked }); - break; - - case 'onlyFollowFirstParent': - onChangeGraphConfiguration?.({ onlyFollowFirstParent: checked }); - break; - - case 'remotes': - case 'stashes': - case 'tags': { - const key = $el.value satisfies keyof GraphExcludeTypes; - const currentFilter = excludeTypes?.[key]; - if ((currentFilter == null && checked) || (currentFilter != null && currentFilter !== checked)) { - setExcludeTypes({ ...excludeTypes, [key]: checked }); - onChangeExcludeTypes?.(key, checked); - } - break; - } - } - }; - - const handleBranchesVisibility = (e: SlChangeEvent): void => { - const $el = e.target as HTMLSelectElement; - if ($el == null) return; - - onChangeRefIncludes?.($el.value as GraphBranchesVisibility); - }; - - const handleMissingAvatars = (emails: GraphAvatars) => { - onMissingAvatars?.(emails); - }; - - const handleMissingRefsMetadata = (metadata: GraphMissingRefsMetadata) => { - onMissingRefsMetadata?.(metadata); - }; - - const handleToggleColumnSettings = (event: React.MouseEvent) => { - const e = event.nativeEvent; - const evt = new MouseEvent('contextmenu', { - bubbles: true, - clientX: e.clientX, - clientY: e.clientY, - }); - e.target?.dispatchEvent(evt); - e.stopImmediatePropagation(); - }; - - const handleMoreCommits = () => { - setIsLoading(true); - onMoreRows?.(); - }; - - const handleOnColumnResized = (columnName: GraphColumnName, columnSettings: GraphColumnSetting) => { - if (columnSettings.width) { - onChangeColumns?.({ - [columnName]: { - width: columnSettings.width, - isHidden: columnSettings.isHidden, - mode: columnSettings.mode as GraphColumnMode, - order: columnSettings.order, - }, - }); - } - }; - - const handleOnGraphVisibleRowsChanged = (top: GraphRow, bottom: GraphRow) => { - setVisibleDays({ - top: new Date(top.date).setHours(23, 59, 59, 999), - bottom: new Date(bottom.date).setHours(0, 0, 0, 0), - }); - }; - - const handleOnGraphColumnsReOrdered = (columnsSettings: GraphColumnsSettings) => { - const graphColumnsConfig: GraphColumnsConfig = {}; - for (const [columnName, config] of Object.entries(columnsSettings as GraphColumnsConfig)) { - graphColumnsConfig[columnName] = { ...config }; - } - onChangeColumns?.(graphColumnsConfig); - }; - - // dirty trick to avoid mutations on the GraphContainer side - const fixedExcludeRefsById = useMemo(() => ({ ...excludeRefsById }), [excludeRefsById]); - const handleOnToggleRefsVisibilityClick = (_event: any, refs: GraphRefOptData[], visible: boolean) => { - if (!visible) { - document.getElementById('hiddenRefs')?.animate( - [ - { offset: 0, background: 'transparent' }, - { - offset: 0.4, - background: 'var(--vscode-statusBarItem-warningBackground)', - }, - { offset: 1, background: 'transparent' }, - ], - { - duration: 1000, - iterations: !Object.keys(fixedExcludeRefsById ?? {}).length ? 2 : 1, - }, - ); - } - onChangeRefsVisibility?.(refs, visible); - }; - - const handleOnDoubleClickRef = ( - _event: React.MouseEvent, - refGroup: GraphRefGroup, - _row: GraphRow, - metadata?: GraphRefMetadataItem, - ) => { - if (refGroup.length > 0) { - onDoubleClickRef?.(refGroup[0], metadata); - } - }; - - const handleOnDoubleClickRow = ( - _event: React.MouseEvent, - graphZoneType: GraphZoneType, - row: GraphRow, - ) => { - if (graphZoneType === refZone) return; - - onDoubleClickRow?.(row, true); - }; - - const handleRowContextMenu = (_event: React.MouseEvent, graphZoneType: GraphZoneType, graphRow: GraphRow) => { - if (graphZoneType === refZone) return; - hover.current?.hide(); - - // If the row is in the current selection, use the typed selection context, otherwise clear it - const newSelectionContext = selectionContexts?.selectedShas.has(graphRow.sha) - ? selectionContexts.contexts.get(graphRow.type) - : emptySelectionContext; - - setContext({ - ...context, - graph: { - ...(context?.graph != null && typeof context.graph === 'string' - ? JSON.parse(context.graph) - : context?.graph), - ...newSelectionContext, - }, - }); - }; - - const computeSelectionContext = (_active: GraphRow, rows: GraphRow[]) => { - if (rows.length <= 1) { - setSelectionContexts(undefined); - return; - } - - const selectedShas = new Set(); - for (const row of rows) { - selectedShas.add(row.sha); - } - - // Group the selected rows by their type and only include ones that have row context - const grouped = groupByFilterMap( - rows, - r => r.type, - r => - r.contexts?.row != null - ? ((typeof r.contexts.row === 'string' - ? JSON.parse(r.contexts.row) - : r.contexts.row) as GraphItemContext) - : undefined, - ); - - const contexts: SelectionContexts['contexts'] = new Map(); - - for (let [type, items] of grouped) { - let webviewItems: string | undefined; - - const contextValues = new Set(); - for (const item of items) { - contextValues.add(item.webviewItem); - } - - if (contextValues.size === 1) { - webviewItems = first(contextValues); - } else if (contextValues.size > 1) { - // If there are multiple contexts, see if they can be boiled down into a least common denominator set - // Contexts are of the form `gitlens:++...`, can also contain multiple `:`, but assume the whole thing is the type - - const itemTypes = new Map>(); - - for (const context of contextValues) { - const [type, ...adds] = context.split('+'); - - let additionalContext = itemTypes.get(type); - if (additionalContext == null) { - additionalContext ??= new Map(); - itemTypes.set(type, additionalContext); - } - - // If any item has no additional context, then only the type is able to be used - if (adds.length === 0) { - additionalContext.clear(); - break; - } - - for (const add of adds) { - additionalContext.set(add, (additionalContext.get(add) ?? 0) + 1); - } - } - - if (itemTypes.size === 1) { - let additionalContext; - [webviewItems, additionalContext] = first(itemTypes)!; - - if (additionalContext.size > 0) { - const commonContexts = join( - filterMap(additionalContext, ([context, count]) => - count === items.length ? context : undefined, - ), - '+', - ); - - if (commonContexts) { - webviewItems += `+${commonContexts}`; - } - } - } else { - // If we have more than one type, something is wrong with our context key setup -- should NOT happen at runtime - debugger; - webviewItems = undefined; - items = []; - } - } - - const count = items.length; - contexts.set(type, { - listDoubleSelection: count === 2, - listMultiSelection: count > 1, - webviewItems: webviewItems, - webviewItemsValues: count > 1 ? items : undefined, - }); - } - - setSelectionContexts({ contexts: contexts, selectedShas: selectedShas }); - }; - - const handleSelectGraphRows = (rows: GraphRow[]) => { - hover.current?.hide(); - - const active = rows[rows.length - 1]; - const activeKey = active != null ? `${active.sha}|${active.date}` : undefined; - // HACK: Ensure the main state is updated since it doesn't come from the extension - state.activeRow = activeKey; - setActiveRow(activeKey); - setActiveDay(active?.date); - computeSelectionContext(active, rows); - - onChangeSelection?.(rows); - }; - - return ( - <> -
-
-
- {repo?.provider?.url && ( - <> - - - emitTelemetrySentEvent<'graph/action/openRepoOnRemote'>(e.target, { - name: 'graph/action/openRepoOnRemote', - data: {}, - }) - } - > - - - - - Open Repository on {repo.provider.name} -
- {repo.provider.integration?.connected ? ( - - - ) : ( - repo.provider.integration?.connected === false && ( - <> -
-
- {repo?.provider?.integration?.connected === false && ( - ( - 'gitlens.plus.cloudIntegrations.connect', - { - integrationIds: [repo.provider.integration.id], - source: 'graph', - }, - )} - > - - - Connect to {repo.provider.name} -
- View pull requests and issues in the Commit Graph, Launchpad, autolinks, and - more -
-
- )} - - )} - - - Switch to Another Repository... - - {allowed && repo && ( - <> - - - - {branchState?.pr && ( - - -
- - branchState.pr?.id ? onOpenPullRequest?.(branchState.pr) : undefined - } - /> -
-
- )} - - - {!branchState?.pr ? ( - branchState?.worktree ? ( - -
- - Switch to Another Branch... -
-
-
-
- - - - Jump to HEAD -
- [Alt] Jump to Reference... -
-
- - - - - - )} -
-
- - (GlCommand.GitCommandsBranch, { - state: { - subcommand: 'create', - reference: branch, - }, - command: 'branch', - confirm: true, - })} - > - - - - Create New Branch from - - {branchName} - - - - ), - )}`} - className="action-button" - > - - - - - Launchpad — organizes your pull requests into actionable - groups to help you focus and keep your team unblocked - - - - - - - - - - GitLens Home — track, manage, and collaborate on your branches and pull - requests, all in one intuitive hub - - - {(subscription == null || !isSubscriptionPaid(subscription)) && ( - - )} -
-
- {allowed && - workingTreeStats != null && - (workingTreeStats.hasConflicts || workingTreeStats.pausedOpStatus) && ( -
- -
- )} - {allowed && ( -
-
- - - - - All Branches - - - Smart Branches - {!repo?.isVirtual ? ( - - - - Shows only relevant branches -
-
- - Includes the current branch, its upstream, and its base or - target branch - -
-
- ) : ( - - )} -
- Current Branch -
-
-
- - - - Hidden Branches / Tags - -
- Hidden Branches / Tags - {excludeRefsById && - Object.keys(excludeRefsById).length && - [...Object.values(excludeRefsById), null].map(ref => - ref ? ( - { - handleOnToggleRefsVisibilityClick(event, [ref], true); - }} - className="flex-gap" - > - - {ref.name} - - ) : ( - // One more weird case. If I render it outside the listed items, the dropdown is hidden after click on the last item - { - handleOnToggleRefsVisibilityClick( - event, - Object.values(excludeRefsById ?? {}), - true, - ); - }} - > - Show All - - ), - )} -
-
-
- - - - Graph Filtering - -
- Graph Filters - {repo?.isVirtual !== true && ( - <> - - - - Simplify Merge History - - - - - - - Hide Remote-only Branches - - - - - Hide Stashes - - - - )} - - - Hide Tags - - - - - - Dim Merge Commit Rows - - -
-
- - - - 2)} - more={searchResults?.paging?.hasMore ?? false} - searching={searching} - filter={state.defaultSearchMode === 'filter'} - value={searchQuery?.query ?? ''} - errorMessage={searchResultsError?.error ?? ''} - resultsHidden={searchResultsHidden} - resultsLoaded={searchResults != null} - onChange={e => handleSearchInput(e)} - onNavigate={e => handleSearchNavigation(e)} - onOpenInView={() => handleSearchOpenInView()} - onSearchModeChange={e => handleSearchModeChange(e)} - /> - - - - - - - Toggle Minimap - - - - - Minimap Options - -
- Minimap - - - - Commits - - - Lines Changed - - - - - Markers - - - - Local Branches - - - - - - Remote Branches - - - - - - Pull Requests - - - - - - Stashes - - - - - - Tags - - -
-
-
-
-
- )} -
-
-
-
- -

- Commit Graph - {' '} - — easily visualize your repository and keep track of all work in progress. Use the rich commit - search to find a specific commit, message, author, a changed file or files, or even a specific code - change. -

-
- handleOnMinimapDaySelected(e)} - > - -
- - {repo !== undefined ? ( - <> - } - cssVariables={styleProps?.cssVariables} - dimMergeCommits={graphConfig?.dimMergeCommits} - downstreamsByUpstream={downstreams} - enabledRefMetadataTypes={graphConfig?.enabledRefMetadataTypes} - enabledScrollMarkerTypes={graphConfig?.scrollMarkerTypes} - enableShowHideRefsOptions - enableMultiSelection={graphConfig?.enableMultiSelection} - excludeRefsById={excludeRefsById} - excludeByType={excludeTypes} - formatCommitDateTime={getGraphDateFormatter(graphConfig)} - getExternalIcon={getIconElementLibrary} - graphRows={rows} - hasMoreCommits={pagingHasMore} - // Just cast the { [id: string]: number } object to { [id: string]: boolean } for performance - highlightedShas={searchResults?.ids as GraphContainerProps['highlightedShas']} - highlightRowsOnRefHover={graphConfig?.highlightRowsOnRefHover} - includeOnlyRefsById={includeOnlyRefsById} - scrollRowPadding={graphConfig?.scrollRowPadding} - showGhostRefsOnRowHover={graphConfig?.showGhostRefsOnRowHover} - showRemoteNamesOnRefs={graphConfig?.showRemoteNamesOnRefs} - isContainerWindowFocused={windowFocused} - isLoadingRows={isLoading} - isSelectedBySha={selectedRows} - nonce={nonce} - onColumnResized={handleOnColumnResized} - onDoubleClickGraphRow={handleOnDoubleClickRow} - onDoubleClickGraphRef={handleOnDoubleClickRef} - onGraphColumnsReOrdered={handleOnGraphColumnsReOrdered} - onGraphMouseLeave={handleOnGraphMouseLeave} - onGraphRowHovered={handleOnGraphRowHovered} - onGraphRowUnhovered={handleOnGraphRowUnhovered} - onRowContextMenu={handleRowContextMenu} - onSettingsClick={handleToggleColumnSettings} - onSelectGraphRows={handleSelectGraphRows} - onToggleRefsVisibilityClick={handleOnToggleRefsVisibilityClick} - onEmailsMissingAvatarUrls={handleMissingAvatars} - onRefsMissingMetadata={handleMissingRefsMetadata} - onShowMoreCommits={handleMoreCommits} - onGraphVisibleRowsChanged={minimap.current ? handleOnGraphVisibleRowsChanged : undefined} - platform={clientPlatform} - refMetadataById={refsMetadata} - rowsStats={rowsStats} - rowsStatsLoading={rowsStatsLoading} - searchMode={searchQuery?.filter ? 'filter' : 'normal'} - shaLength={graphConfig?.idLength} - shiftSelectMode="simple" - suppressNonRefRowTooltips - themeOpacityFactor={styleProps?.themeOpacityFactor} - useAuthorInitialsForAvatars={!graphConfig?.avatars} - workDirStats={workingTreeStats} - /> - - ) : ( -

No repository is selected

- )} -
- - ); -} - -function formatCommitDateTime( - date: number, - style: DateStyle = 'absolute', - format: DateTimeFormat | string = 'short+short', - source?: CommitDateTimeSources, -): string { - switch (source) { - case CommitDateTimeSources.Tooltip: - return `${formatDate(date, format)} (${fromNow(date)})`; - case CommitDateTimeSources.RowEntry: - default: - return style === 'relative' ? fromNow(date) : formatDate(date, format); - } -} - -function getClosestSearchResultIndex( - results: GraphSearchResults, - query: SearchQuery | undefined, - activeRow: string | undefined, - next: boolean = true, -): [number, string | undefined] { - if (results.ids == null) return [0, undefined]; - - const activeInfo = getActiveRowInfo(activeRow); - const activeId = activeInfo?.id; - if (activeId == null) return [0, undefined]; - - let index: number | undefined; - let nearestId: string | undefined; - let nearestIndex: number | undefined; - - const data = results.ids[activeId]; - if (data != null) { - index = data.i; - nearestId = activeId; - nearestIndex = index; - } - - if (index == null) { - const activeDate = activeInfo?.date != null ? activeInfo.date + (next ? 1 : -1) : undefined; - if (activeDate == null) return [0, undefined]; - - // Loop through the search results and: - // try to find the active id - // if next=true find the nearest date before the active date - // if next=false find the nearest date after the active date - - let i: number; - let id: string; - let date: number; - let nearestDate: number | undefined; - for ([id, { date, i }] of Object.entries(results.ids)) { - if (next) { - if (date < activeDate && (nearestDate == null || date > nearestDate)) { - nearestId = id; - nearestDate = date; - nearestIndex = i; - } - } else if (date > activeDate && (nearestDate == null || date <= nearestDate)) { - nearestId = id; - nearestDate = date; - nearestIndex = i; - } - } - - index = nearestIndex == null ? results.count - 1 : nearestIndex + (next ? -1 : 1); - } - - index = getNextOrPreviousSearchResultIndex(index, next, results, query); - - return index === nearestIndex ? [index, nearestId] : [index, undefined]; -} - -function getNextOrPreviousSearchResultIndex( - index: number, - next: boolean, - results: GraphSearchResults, - query: SearchQuery | undefined, -) { - if (next) { - if (index < results.count - 1) { - index++; - } else if (query != null && results?.paging?.hasMore) { - index = -1; // Indicates a boundary that we should load more results - } else { - index = 0; - } - } else if (index > 0) { - index--; - } else if (query != null && results?.paging?.hasMore) { - index = -1; // Indicates a boundary that we should load more results - } else { - index = results.count - 1; - } - return index; -} - -function getSearchResultIdByIndex(results: GraphSearchResults, index: number): string | undefined { - // Loop through the search results without using Object.entries or Object.keys and return the id at the specified index - const { ids } = results; - for (const id in ids) { - if (ids[id].i === index) return id; - } - return undefined; - - // return Object.entries(results.ids).find(([, { i }]) => i === index)?.[0]; -} - -function getActiveRowInfo(activeRow: string | undefined): { id: string; date: number } | undefined { - if (activeRow == null) return undefined; - - const [id, date] = activeRow.split('|'); - return { - id: id, - date: Number(date), - }; -} - -function getSearchResultModel(state: State): { - results: GraphSearchResults | undefined; - resultsError: GraphSearchResultsError | undefined; -} { - let results: GraphSearchResults | undefined; - let resultsError: GraphSearchResultsError | undefined; - if (state.searchResults != null) { - if ('error' in state.searchResults) { - resultsError = state.searchResults; - } else { - results = state.searchResults; - } - } - return { results: results, resultsError: resultsError }; -} diff --git a/src/webviews/apps/plus/graph/actions/gitActionsButtons.tsx b/src/webviews/apps/plus/graph/actions/gitActionsButtons.tsx index ee4436ac5eec6..10b83bcd1076a 100644 --- a/src/webviews/apps/plus/graph/actions/gitActionsButtons.tsx +++ b/src/webviews/apps/plus/graph/actions/gitActionsButtons.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { fromNow } from '../../../../../system/date'; import type { BranchState, State } from '../../../../plus/graph/protocol'; import { FetchButton } from './fetchButton'; @@ -18,7 +18,25 @@ export const GitActionsButtons = ({ const remote = branchState?.upstream ? {branchState?.upstream} : 'remote'; const lastFetchedDate = lastFetched && new Date(lastFetched); - const fetchedText = lastFetchedDate && lastFetchedDate.getTime() !== 0 ? fromNow(lastFetchedDate) : undefined; + const [fetchedText, setFetchedText] = useState( + lastFetchedDate && lastFetchedDate.getTime() !== 0 ? fromNow(lastFetchedDate) : undefined, + ); + useEffect(() => { + if (!lastFetchedDate) { + return; + } + const deltaSeconds = (new Date().getTime() - lastFetchedDate.getTime()) / 1000; + const delay = deltaSeconds < 60 ? 1000 : deltaSeconds < 60 * 60 ? 60000 : undefined; + if (!delay) { + return; + } + const timeout = setTimeout(() => { + setFetchedText(fromNow(lastFetchedDate)); + }, delay); + return () => { + clearTimeout(timeout); + }; + }, [lastFetchedDate]); return ( <> diff --git a/src/webviews/apps/plus/graph/actions/gitActionsButtons.wc.ts b/src/webviews/apps/plus/graph/actions/gitActionsButtons.wc.ts new file mode 100644 index 0000000000000..ac6327663481a --- /dev/null +++ b/src/webviews/apps/plus/graph/actions/gitActionsButtons.wc.ts @@ -0,0 +1,13 @@ +import r2wc from '@r2wc/react-to-web-component'; +import { GitActionsButtons } from './gitActionsButtons'; + +const GitActionsButtonsWC = r2wc(GitActionsButtons, { + props: { + branchName: 'string', + branchState: 'string', + lastFetched: 'json', + state: 'json', + }, +}); + +customElements.define('gl-git-actions-buttons', GitActionsButtonsWC); diff --git a/src/webviews/apps/plus/graph/context.ts b/src/webviews/apps/plus/graph/context.ts new file mode 100644 index 0000000000000..afe1f15d3c0f2 --- /dev/null +++ b/src/webviews/apps/plus/graph/context.ts @@ -0,0 +1,4 @@ +import { createContext } from '@lit/context'; +import type { State } from '../../../plus/graph/protocol'; + +export const stateContext = createContext('state'); diff --git a/src/webviews/apps/plus/graph/graph-app.ts b/src/webviews/apps/plus/graph/graph-app.ts new file mode 100644 index 0000000000000..afbbd56cf6ea3 --- /dev/null +++ b/src/webviews/apps/plus/graph/graph-app.ts @@ -0,0 +1,165 @@ +import { consume } from '@lit/context'; +import { SignalWatcher } from '@lit-labs/signals'; +import '@shoelace-style/shoelace/dist/components/option/option.component.js'; +import '@shoelace-style/shoelace/dist/components/select/select.component.js'; +import { html, LitElement } from 'lit'; +import { customElement, query } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { GlCommand } from '../../../../constants.commands'; +import { createWebviewCommandLink } from '../../../../system/webview'; +import '../../shared/components/branch-icon'; +import '../../shared/components/button'; +import '../../shared/components/code-icon'; +import type { CustomEventType } from '../../shared/components/element'; +import '../../shared/components/feature-badge'; +import '../../shared/components/feature-gate'; +import '../../shared/components/menu'; +import '../../shared/components/overlays/popover'; +import '../../shared/components/overlays/tooltip'; +import '../../shared/components/rich/issue-pull-request'; +import { emitTelemetrySentEvent } from '../../shared/telemetry'; +import '../shared/components/merge-rebase-status'; +import './actions/gitActionsButtons.wc'; +import { stateContext } from './context'; +import './graph-header'; +import type { GLGraphWrapper } from './graph-wrapper/graph-wrapper'; +import './graph.scss'; +import type { GlGraphHover } from './hover/graphHover'; +import type { GraphMinimapDaySelectedEventDetail } from './minimap/minimap'; +import type { GlGraphMinimapContainer } from './minimap/minimap-container'; +import './sidebar/sidebar'; +import { graphStateContext } from './stateProvider'; + +@customElement('gl-graph-app-wc') +export class GraphAppWC extends SignalWatcher(LitElement) { + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this; + } + + @consume({ context: stateContext, subscribe: true }) + state!: typeof stateContext.__context__; + + @consume({ context: graphStateContext, subscribe: true }) + graphApp!: typeof graphStateContext.__context__; + + @query('gl-graph-minimap-container') + minimapEl!: GlGraphMinimapContainer; + + @query('gl-graph-wrapper') + graphEl!: GLGraphWrapper; + + @query('gl-graph-wrapper') + graphWrapper!: GLGraphWrapper; + + private handleHeaderSearchNavigation(e: CustomEventType<'gl-select-commits'>) { + this.graphWrapper.selectCommits([e.detail], false, true); + } + + private handleMinimapDaySelected(e: CustomEvent) { + if (!this.state.rows) { + return; + } + let { sha } = e.detail; + if (sha == null) { + const date = e.detail.date?.getTime(); + if (date == null) return; + + // Find closest row to the date + const closest = this.state.rows.reduce((prev, curr) => + Math.abs(curr.date - date) < Math.abs(prev.date - date) ? curr : prev, + ); + sha = closest.sha; + } + + this.graphEl.selectCommits([sha], false, true); + + queueMicrotask( + () => + e.target && + emitTelemetrySentEvent<'graph/minimap/day/selected'>(e.target, { + name: 'graph/minimap/day/selected', + data: {}, + }), + ); + } + + private handleGraphVisibleDaysChanged(e: CustomEventType<'gl-graph-change-visible-days'>) { + this.graphApp.visibleDays = e.detail; + } + + private handleGraphRowHovered(e: CustomEventType<'gl-graph-hovered-row'>) { + this.minimapEl.select(e.detail.graphRow.date, true); + } + + private handleGraphMouseLeaved() { + this.minimapEl.unselect(undefined, true); + } + + resetHover() { + this.hoverElement.reset(); + } + + @query('gl-graph-hover') + private readonly hoverElement!: GlGraphHover; + + override render() { + return html` + + +

+ Commit Graph + + — easily visualize your repository and keep track of all work in progress. Use the rich commit + search to find a specific commit, message, author, a changed file or files, or even a specific code + change. +

+
+
+ +
`; + } +} + +new GraphAppWC(); diff --git a/src/webviews/apps/plus/graph/graph-header.ts b/src/webviews/apps/plus/graph/graph-header.ts new file mode 100644 index 0000000000000..62a59caa63007 --- /dev/null +++ b/src/webviews/apps/plus/graph/graph-header.ts @@ -0,0 +1,1160 @@ +import type { GraphRefOptData } from '@gitkraken/gitkraken-components'; +import { consume } from '@lit/context'; +import { SignalWatcher } from '@lit-labs/signals'; +import '@shoelace-style/shoelace/dist/components/option/option.js'; +import '@shoelace-style/shoelace/dist/components/select/select.js'; +import { html, LitElement, nothing } from 'lit'; +import { customElement, query } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { when } from 'lit/directives/when.js'; +import type { ConnectCloudIntegrationsCommandArgs } from '../../../../commands/cloudIntegrations'; +import type { BranchGitCommandArgs } from '../../../../commands/git/branch'; +import type { GraphBranchesVisibility } from '../../../../config'; +import { GlCommand } from '../../../../constants.commands'; +import type { SearchQuery } from '../../../../constants.search'; +import { isSubscriptionPaid } from '../../../../plus/gk/utils/subscription.utils'; +import type { LaunchpadCommandArgs } from '../../../../plus/launchpad/launchpad'; +import { createCommandLink } from '../../../../system/commands'; +import { debounced } from '../../../../system/function'; +import { createWebviewCommandLink } from '../../../../system/webview'; +import type { + GraphExcludedRef, + GraphExcludeTypes, + GraphMinimapMarkerTypes, + GraphRepository, + GraphSearchResults, + State, + UpdateGraphConfigurationParams, +} from '../../../plus/graph/protocol'; +import { + ChooseRefRequest, + ChooseRepositoryCommand, + EnsureRowRequest, + OpenPullRequestDetailsCommand, + SearchOpenInViewCommand, + SearchRequest, + UpdateExcludeTypesCommand, + UpdateGraphConfigurationCommand, + UpdateGraphSearchModeCommand, + UpdateIncludedRefsCommand, + UpdateRefsVisibilityCommand, +} from '../../../plus/graph/protocol'; +import '../../shared/components/branch-icon'; +import '../../shared/components/button'; +import '../../shared/components/checkbox/checkbox'; +import '../../shared/components/code-icon'; +import type { CustomEventType } from '../../shared/components/element'; +import '../../shared/components/indicators/indicator'; +import '../../shared/components/menu'; +import '../../shared/components/overlays/popover'; +import '../../shared/components/overlays/tooltip'; +import '../../shared/components/radio/radio'; +import '../../shared/components/radio/radio-group'; +import type { RadioGroup } from '../../shared/components/radio/radio-group'; +import '../../shared/components/rich/issue-pull-request'; +import type { GlSearchBox } from '../../shared/components/search/search-box'; +import '../../shared/components/search/search-box'; +import type { SearchNavigationEventDetail } from '../../shared/components/search/search-input'; +import type { TelemetryContext } from '../../shared/context'; +import { ipcContext, telemetryContext } from '../../shared/context'; +import { emitTelemetrySentEvent } from '../../shared/telemetry'; +import '../shared/components/merge-rebase-status'; +import './actions/gitActionsButtons.wc'; +import { stateContext } from './context'; +import './graph-wrapper/graph-wrapper'; +import './graph.scss'; +import { graphStateContext } from './stateProvider'; + +declare global { + interface HTMLElementTagNameMap { + 'gl-graph-header': GlGraphHeader; + } + + interface GlobalEventHandlersEventMap { + 'gl-select-commits': CustomEvent; + } +} + +function getRemoteIcon(type: number | string) { + switch (type) { + case 'head': + return 'vm'; + case 'remote': + return 'cloud'; + case 'tag': + return 'tag'; + default: + return ''; + } +} + +function getSearchResultIdByIndex(results: GraphSearchResults, index: number): string | undefined { + // Loop through the search results without using Object.entries or Object.keys and return the id at the specified index + const { ids } = results; + for (const id in ids) { + if (ids[id].i === index) return id; + } + return undefined; + + // return Object.entries(results.ids).find(([, { i }]) => i === index)?.[0]; +} + +@customElement('gl-graph-header') +export class GlGraphHeader extends SignalWatcher(LitElement) { + // use Light DOM + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this; + } + + @consume({ context: ipcContext }) + _ipc!: typeof ipcContext.__context__; + + @consume({ context: telemetryContext as { __context__: TelemetryContext } }) + _telemetry!: TelemetryContext; + + @consume({ context: stateContext, subscribe: true }) + hostState!: typeof stateContext.__context__; + + @consume({ context: graphStateContext }) + appState!: typeof graphStateContext.__context__; + + get hasFilters() { + if (this.hostState.config?.onlyFollowFirstParent) return true; + if (this.hostState.excludeTypes == null) return false; + + return Object.values(this.hostState.excludeTypes).includes(true); + } + + private async onJumpToRefPromise(alt: boolean): Promise<{ name: string; sha: string } | undefined> { + try { + // Assuming we have a command to get the ref details + const rsp = await this._ipc.sendRequest(ChooseRefRequest, { alt: alt }); + this._telemetry.sendEvent({ name: 'graph/action/jumpTo', data: { alt: alt } }); + return rsp; + } catch { + return undefined; + } + } + + private async handleJumpToRef(e: MouseEvent) { + const ref = await this.onJumpToRefPromise(e.altKey); + if (ref != null) { + const sha = await this.ensureSearchResultRow(ref.sha); + if (sha == null) return; + + this.dispatchEvent(new CustomEvent('gl-select-commits', { detail: sha })); + } + } + + private onOpenPullRequest(pr: NonNullable['pr']>): void { + this._ipc.sendCommand(OpenPullRequestDetailsCommand, { id: pr.id }); + } + + private onSearchOpenInView() { + this._ipc.sendCommand(SearchOpenInViewCommand, { search: { ...this.appState.filter } }); + } + + private onExcludeTypesChanged(key: keyof GraphExcludeTypes, value: boolean) { + this._ipc.sendCommand(UpdateExcludeTypesCommand, { key: key, value: value }); + } + + private onRefIncludesChanged(branchesVisibility: GraphBranchesVisibility, refs?: GraphRefOptData[]) { + this._ipc.sendCommand(UpdateIncludedRefsCommand, { branchesVisibility: branchesVisibility, refs: refs }); + } + + private getActiveRowInfo(): undefined | { date: number; id: string } { + if (this.appState.activeRow == null) return undefined; + + const [id, date] = this.appState.activeRow.split('|'); + return { + date: Number(date), + id: id, + }; + } + + private getNextOrPreviousSearchResultIndex( + index: number, + next: boolean, + results: GraphSearchResults, + query: undefined | SearchQuery, + ) { + if (next) { + if (index < results.count - 1) { + index++; + } else if (query != null && results?.paging?.hasMore) { + index = -1; // Indicates a boundary that we should load more results + } else { + index = 0; + } + } else if (index > 0) { + index--; + } else if (query != null && results?.paging?.hasMore) { + index = -1; // Indicates a boundary that we should load more results + } else { + index = results.count - 1; + } + return index; + } + + private getClosestSearchResultIndex( + results: GraphSearchResults, + query: undefined | SearchQuery, + next: boolean = true, + ): [number, undefined | string] { + if (results.ids == null) return [0, undefined]; + + const activeInfo = this.getActiveRowInfo(); + const activeId = activeInfo?.id; + if (activeId == null) return [0, undefined]; + + let index: undefined | number; + let nearestId: undefined | string; + let nearestIndex: undefined | number; + + const data = results.ids[activeId]; + if (data != null) { + index = data.i; + nearestId = activeId; + nearestIndex = index; + } + + if (index == null) { + const activeDate = activeInfo?.date != null ? activeInfo.date + (next ? 1 : -1) : undefined; + if (activeDate == null) return [0, undefined]; + + // Loop through the search results and: + // try to find the active id + // if next=true find the nearest date before the active date + // if next=false find the nearest date after the active date + + let i: number; + let id: string; + let date: number; + let nearestDate: undefined | number; + for ([id, { date, i }] of Object.entries(results.ids)) { + if (next) { + if (date < activeDate && (nearestDate == null || date > nearestDate)) { + nearestId = id; + nearestDate = date; + nearestIndex = i; + } + } else if (date > activeDate && (nearestDate == null || date <= nearestDate)) { + nearestId = id; + nearestDate = date; + nearestIndex = i; + } + } + + index = nearestIndex == null ? results.count - 1 : nearestIndex + (next ? -1 : 1); + } + + index = this.getNextOrPreviousSearchResultIndex(index, next, results, query); + + return index === nearestIndex ? [index, nearestId] : [index, undefined]; + } + + private get searchPosition(): number { + if (this.appState.searchResults?.ids == null || !this.appState.filter.query) return 0; + + const id = this.getActiveRowInfo()?.id; + let searchIndex = id ? this.appState.searchResults.ids[id]?.i : undefined; + if (searchIndex == null) { + [searchIndex] = this.getClosestSearchResultIndex(this.appState.searchResults, { ...this.appState.filter }); + } + return searchIndex < 1 ? 1 : searchIndex + 1; + } + + get searchValid() { + return this.appState.filter.query.length > 2; + } + handleFilterChange(e: CustomEvent) { + const $el = e.target as HTMLInputElement; + if ($el == null) return; + + const { checked } = $el; + + switch ($el.value) { + case 'mergeCommits': + this.changeGraphConfiguration({ dimMergeCommits: checked }); + break; + + case 'onlyFollowFirstParent': + this.changeGraphConfiguration({ onlyFollowFirstParent: checked }); + break; + + case 'remotes': + case 'stashes': + case 'tags': { + const key = $el.value satisfies keyof GraphExcludeTypes; + const currentFilter = this.hostState.excludeTypes?.[key]; + if ((currentFilter == null && checked) || (currentFilter != null && currentFilter !== checked)) { + this.onExcludeTypesChanged(key, checked); + } + break; + } + } + } + handleOnToggleRefsVisibilityClick(_event: any, refs: GraphExcludedRef[], visible: boolean) { + this._ipc.sendCommand(UpdateRefsVisibilityCommand, { + refs: refs, + visible: visible, + }); + } + handleBranchesVisibility(e: CustomEvent) { + const $el = e.target as HTMLSelectElement; + if ($el == null) return; + this.onRefIncludesChanged($el.value as GraphBranchesVisibility); + } + + @debounced(250) + async handleSearch() { + this.appState.searching = this.searchValid; + try { + const rsp = await this._ipc.sendRequest(SearchRequest, { + search: this.searchValid ? { ...this.appState.filter } : undefined /*limit: options?.limit*/, + }); + + if (rsp.results && this.appState.filter.query) { + this.searchEl.logSearch({ ...this.appState.filter }); + } + + this.appState.searchResultsResponse = rsp.results; + this.appState.selectedRows = rsp.selectedRows; + } catch { + this.appState.searchResultsResponse = undefined; + } + this.appState.searching = false; + } + + private handleSearchInput = (e: CustomEvent) => { + this.appState.filter = e.detail; + void this.handleSearch(); + }; + + private async onSearchPromise(search: SearchQuery, options?: { limit?: number; more?: boolean }) { + try { + const rsp = await this._ipc.sendRequest(SearchRequest, { + search: search, + limit: options?.limit, + more: options?.more, + }); + this.appState.searchResultsResponse = rsp.results; + this.appState.selectedRows = rsp.selectedRows; + return rsp; + } catch { + return undefined; + } + } + + private async handleSearchNavigation(e: CustomEvent) { + let results = this.appState.searchResults; + if (results == null) return; + + const direction = e.detail?.direction ?? 'next'; + + let count = results.count; + + let searchIndex; + let id: string | undefined; + + let next; + if (direction === 'first') { + next = false; + searchIndex = 0; + } else if (direction === 'last') { + next = false; + searchIndex = -1; + } else { + next = direction === 'next'; + [searchIndex, id] = this.getClosestSearchResultIndex(results, { ...this.appState.filter }, next); + } + + let iterations = 0; + // Avoid infinite loops + while (iterations < 1000) { + iterations++; + + // Indicates a boundary and we need to load more results + if (searchIndex === -1) { + if (next) { + if (this.appState.filter.query && results?.paging?.hasMore) { + this.appState.searching = true; + let moreResults; + try { + moreResults = await this.onSearchPromise?.({ ...this.appState.filter }, { more: true }); + } finally { + this.appState.searching = false; + } + if (moreResults?.results != null && !('error' in moreResults.results)) { + if (count < moreResults.results.count) { + results = moreResults.results; + searchIndex = count; + count = results.count; + } else { + searchIndex = 0; + } + } else { + searchIndex = 0; + } + } else { + searchIndex = 0; + } + // this.appState.filter != null seems noop + } else if (direction === 'last' && this.appState.filter != null && results?.paging?.hasMore) { + this.appState.searching = true; + let moreResults; + try { + moreResults = await this.onSearchPromise({ ...this.appState.filter }, { limit: 0, more: true }); + } finally { + this.appState.searching = false; + } + if (moreResults?.results != null && !('error' in moreResults.results)) { + if (count < moreResults.results.count) { + results = moreResults.results; + count = results.count; + } + searchIndex = count; + } + } else { + searchIndex = count - 1; + } + } + + id = id ?? getSearchResultIdByIndex(results, searchIndex); + if (id != null) { + id = await this.ensureSearchResultRow(id); + if (id != null) break; + } + + this.appState.searchResultsHidden = true; + + searchIndex = this.getNextOrPreviousSearchResultIndex(searchIndex, next, results, { + ...this.appState.filter, + }); + } + + if (id != null) { + this.dispatchEvent(new CustomEvent('gl-select-commits', { detail: id })); + } + } + + private async onEnsureRowPromise(id: string, select: boolean) { + try { + return await this._ipc.sendRequest(EnsureRowRequest, { id: id, select: select }); + } catch { + return undefined; + } + } + + private readonly ensuredIds = new Set(); + private readonly ensuredSkippedIds = new Set(); + + private async ensureSearchResultRow(id: string): Promise { + if (this.ensuredIds.has(id)) return id; + if (this.ensuredSkippedIds.has(id)) return undefined; + + let timeout: ReturnType | undefined = setTimeout(() => { + timeout = undefined; + this.appState.loading = true; + }, 500); + + const e = await this.onEnsureRowPromise(id, false); + if (timeout == null) { + this.appState.loading = false; + } else { + clearTimeout(timeout); + } + + if (e?.id === id) { + this.ensuredIds.add(id); + return id; + } + + if (e != null) { + this.ensuredSkippedIds.add(id); + } + return undefined; + } + + handleSearchModeChanged(e: CustomEvent) { + this._ipc.sendCommand(UpdateGraphSearchModeCommand, { searchMode: e.detail.searchMode }); + } + + handleMinimapToggled() { + this.changeGraphConfiguration({ minimap: !this.hostState.config?.minimap }); + } + + private changeGraphConfiguration(changes: UpdateGraphConfigurationParams['changes']) { + this._ipc.sendCommand(UpdateGraphConfigurationCommand, { changes: changes }); + } + + private handleMinimapDataTypeChanged(e: Event) { + if (this.hostState.config == null) return; + + const $el = e.target as RadioGroup; + const minimapDataType = $el.value === 'lines' ? 'lines' : 'commits'; + if (this.hostState.config.minimapDataType === minimapDataType) return; + + this.changeGraphConfiguration({ minimapDataType: minimapDataType }); + } + + private handleMinimapAdditionalTypesChanged(e: Event) { + if (this.hostState.config?.minimapMarkerTypes == null) return; + + const $el = e.target as HTMLInputElement; + const value = $el.value as GraphMinimapMarkerTypes; + + if ($el.checked) { + if (!this.hostState.config.minimapMarkerTypes.includes(value)) { + const minimapMarkerTypes = [...this.hostState.config.minimapMarkerTypes, value]; + this.changeGraphConfiguration({ minimapMarkerTypes: minimapMarkerTypes }); + } + } else { + const index = this.hostState.config.minimapMarkerTypes.indexOf(value); + if (index !== -1) { + const minimapMarkerTypes = [...this.hostState.config.minimapMarkerTypes]; + minimapMarkerTypes.splice(index, 1); + this.changeGraphConfiguration({ minimapMarkerTypes: minimapMarkerTypes }); + } + } + } + + @debounced(250) + private handleChooseRepository() { + this._ipc.sendCommand(ChooseRepositoryCommand); + } + + @query('gl-search-box') + private readonly searchEl!: GlSearchBox; + + private renderBranchStateIcon(): unknown { + const { branchState } = this.hostState; + if (branchState?.pr) { + return nothing; + } + if (branchState?.worktree) { + return html``; + } + return html``; + } + + private renderRepoControl(repo?: GraphRepository) { + return html` + + + emitTelemetrySentEvent<'graph/action/openRepoOnRemote'>(e.target!, { + name: 'graph/action/openRepoOnRemote', + data: {}, + })} + > + + ${when( + repo!.provider!.integration?.connected, + () => html``, + )} + + + + Open Repository on ${repo!.provider!.name} +
+ ${when( + repo!.provider!.integration?.connected, + () => html` + + + Connected to ${repo!.provider!.name} + + `, + () => { + if (repo!.provider!.integration?.connected !== false) { + return nothing; + } + return html` + + ( + 'gitlens.plus.cloudIntegrations.connect', + { + integrationIds: [repo!.provider!.integration.id], + source: 'graph', + }, + )} + > + Connect to ${repo!.provider!.name} + + — not connected + `; + }, + )} +
+
+ ${when( + repo?.provider?.integration?.connected === false, + () => html` + ( + 'gitlens.plus.cloudIntegrations.connect', + { + integrationIds: [repo!.provider!.integration!.id], + source: 'graph', + }, + )} + > + + + Connect to ${repo!.provider!.name} +
+ View pull requests and issues in the Commit Graph, Launchpad, autolinks, and more +
+
+ `, + )} + `; + } + + override render() { + const repo = this.hostState.repositories?.find(repo => repo.id === this.hostState.selectedRepository); + return html`
+
+
+ ${when(repo?.provider?.url, this.renderRepoControl.bind(this, repo))} + + + Switch to Another Repository... + + ${when( + this.hostState.allowed && repo, + () => html` + ${when( + this.hostState.branchState?.pr, + pr => html` + + +
+ { + this.onOpenPullRequest(pr); + }} + > + +
+
+ `, + )} + + + ${this.renderBranchStateIcon()} + ${this.hostState.branch?.name} + + +
+ + Switch to Another Branch... +
+ + ${this.hostState.branch?.name}${when( + this.hostState.branchState?.worktree, + () => html` (in a worktree) `, + )} +
+
+
+ + + + Jump to HEAD +
+ [Alt] Jump to Reference... +
+
+ + + + + `, + )} +
+
+ + (GlCommand.GitCommandsBranch, { + state: { + subcommand: 'create', + reference: this.hostState.branch, + }, + command: 'branch', + confirm: true, + })} + > + + + + Create New Branch from + + ${this.hostState.branch?.name} + + + + ), + )}`} + class="action-button" + > + + + + + Launchpad — organizes your pull requests into actionable groups + to help you focus and keep your team unblocked + + + + + + + + + + + GitLens Home — track, manage, and collaborate on your branches and pull + requests, all in one intuitive hub + + + ${when( + this.hostState.subscription == null || !isSubscriptionPaid(this.hostState.subscription), + () => html` + + `, + )} +
+
+ + ${when( + this.hostState.allowed && + this.hostState.workingTreeStats != null && + (this.hostState.workingTreeStats.hasConflicts || this.hostState.workingTreeStats.pausedOpStatus), + () => html` +
+ +
+ `, + )} + ${when( + this.hostState.allowed, + () => html` +
+
+ + + + All Branches + + Smart Branches + ${when( + !repo?.isVirtual, + () => html` + + + + Shows only relevant branches +
+
+ + Includes the current branch, its upstream, and its base or + target branch + +
+
+ `, + () => html` `, + )} +
+ Current Branch +
+
+
+ + + + Hidden Branches / Tags + +
+ Hidden Branches / Tags + ${when(this.hostState.excludeRefs, excludeRefs => { + if (!Object.keys(excludeRefs).length) { + return nothing; + } + return repeat([...Object.values(excludeRefs), null], ref => { + if (ref) { + return html` { + this.handleOnToggleRefsVisibilityClick(event, [ref], true); + }} + class="flex-gap" + > + + ${ref.name} + `; + } + return html` { + this.handleOnToggleRefsVisibilityClick( + event, + Object.values(excludeRefs ?? {}), + true, + ); + }} + > + Show All + `; + }); + })} +
+
+
+ + + + Graph Filtering + +
+ Graph Filters + ${when( + repo?.isVirtual !== true, + () => html` + + + + Simplify Merge History + + + + + + + Hide Remote-only Branches + + + + + Hide Stashes + + + `, + )} + + + Hide Tags + + + + + + Dim Merge Commit Rows + + +
+
+ + + + ) => + this.handleSearchInput(e)} + @gl-search-navigate=${this.handleSearchNavigation} + @gl-search-openinview=${this.onSearchOpenInView} + @gl-search-modechange=${this.handleSearchModeChanged} + > + + + + + + + Toggle Minimap + + + + + Minimap Options + +
+ Minimap + + + Commits + + Lines Changed + + + + + Markers + + + + Local Branches + + + + + + Remote Branches + + + + + + Pull Requests + + + + + + Stashes + + + + + + Tags + + +
+
+
+
+
+ `, + )} +
+
+
+
`; + } +} + +new GlGraphHeader(); diff --git a/src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.react.tsx b/src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.react.tsx new file mode 100644 index 0000000000000..ece9386f1a4c2 --- /dev/null +++ b/src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.react.tsx @@ -0,0 +1,570 @@ +import type { + CommitType, + ExcludeRefsById, + GetExternalIcon, + GraphColumnMode, + GraphColumnSetting, + GraphColumnsSettings, + GraphContainerProps, + GraphPlatform, + GraphRef, + GraphRefGroup, + GraphRefOptData, + GraphRow, + GraphZoneType, + OnFormatCommitDateTime, +} from '@gitkraken/gitkraken-components'; +import GraphContainer, { CommitDateTimeSources, refZone } from '@gitkraken/gitkraken-components'; +import type { ReactElement } from 'react'; +import React, { createElement, useCallback, useEffect, useMemo, useState } from 'react'; +import { getPlatform } from '@env/platform'; +import type { DateStyle } from '../../../../../config'; +import type { DateTimeFormat } from '../../../../../system/date'; +import { formatDate, fromNow } from '../../../../../system/date'; +import { filterMap, first, groupByFilterMap, join } from '../../../../../system/iterable'; +import type { + GraphAvatars, + GraphColumnName, + GraphColumnsConfig, + GraphComponentConfig, + GraphExcludedRef, + GraphItemContext, + GraphMissingRefsMetadata, + GraphRefMetadataItem, + State, +} from '../../../../plus/graph/protocol'; +import { GlMarkdown } from '../../../shared/components/markdown/markdown.react'; +import type { GraphAppState } from '../stateProvider'; + +export type GraphWrapperProps = Pick< + State, + | 'avatars' + | 'columns' + | 'context' + | 'config' + | 'downstreams' + | 'rows' + | 'excludeRefs' + | 'excludeTypes' + | 'nonce' + | 'paging' + | 'loading' + | 'selectedRows' + | 'windowFocused' + | 'refsMetadata' + | 'includeOnlyRefs' + | 'rowsStats' + | 'rowsStatsLoading' + | 'workingTreeStats' +> & + Pick; + +export interface GraphWrapperEvents { + onGraphMouseLeave?: () => void; + onChangeColumns?: (colsSettings: GraphColumnsConfig) => void; + onChangeRefsVisibility?: (args: { refs: GraphExcludedRef[]; visible: boolean }) => void; + onChangeSelection?: (rows: GraphRow[]) => void; + onDoubleClickRef?: (args: { ref: GraphRef; metadata?: GraphRefMetadataItem }) => void; + onDoubleClickRow?: (args: { row: GraphRow; preserveFocus?: boolean }) => void; + onMissingAvatars?: (emails: Record) => void; + onMissingRefsMetadata?: (metadata: GraphMissingRefsMetadata) => void; + onMoreRows?: (id?: string) => void; + onChangeVisibleDays?: (args: any) => void; + onGraphRowHovered?: (args: { + clientX: number; + currentTarget: HTMLElement; + graphZoneType: GraphZoneType; + graphRow: GraphRow; + }) => void; + onGraphRowUnhovered?: (args: { + relatedTarget: EventTarget | null; + graphZoneType: GraphZoneType; + graphRow: GraphRow; + }) => void; + onRowContextMenu?: (args: { graphZoneType: GraphZoneType; graphRow: GraphRow }) => void; +} + +const getGraphDateFormatter = (config?: GraphComponentConfig): OnFormatCommitDateTime => { + return (commitDateTime: number, source?: CommitDateTimeSources) => + formatCommitDateTime(commitDateTime, config?.dateStyle, config?.dateFormat, source); +}; + +const createIconElements = () => { + const iconList = [ + 'head', + 'remote', + 'remote-github', + 'remote-githubEnterprise', + 'remote-gitlab', + 'remote-gitlabSelfHosted', + 'remote-bitbucket', + 'remote-bitbucketServer', + 'remote-azureDevops', + 'tag', + 'stash', + 'check', + 'loading', + 'warning', + 'added', + 'modified', + 'deleted', + 'renamed', + 'resolved', + 'pull-request', + 'show', + 'hide', + 'branch', + 'graph', + 'commit', + 'author', + 'datetime', + 'message', + 'changes', + 'files', + 'worktree', + 'issue-github', + 'issue-gitlab', + 'issue-jiraCloud', + ]; + + const miniIconList = ['upstream-ahead', 'upstream-behind']; + + const elementLibrary: Record = {}; + iconList.forEach(iconKey => { + elementLibrary[iconKey] = createElement('span', { className: `graph-icon icon--${iconKey}` }); + }); + miniIconList.forEach(iconKey => { + elementLibrary[iconKey] = createElement('span', { className: `graph-icon mini-icon icon--${iconKey}` }); + }); + //TODO: fix this once the styling is properly configured component-side + elementLibrary.settings = createElement('span', { + className: 'graph-icon icon--settings', + style: { fontSize: '1.1rem', right: '0px', top: '-1px' }, + }); + return elementLibrary; +}; + +const iconElementLibrary = createIconElements(); + +const getIconElementLibrary: GetExternalIcon = (iconKey: string) => { + return iconElementLibrary[iconKey]; +}; + +const getClientPlatform = (): GraphPlatform => { + switch (getPlatform()) { + case 'web-macOS': + return 'darwin'; + case 'web-windows': + return 'win32'; + case 'web-linux': + default: + return 'linux'; + } +}; + +const clientPlatform = getClientPlatform(); + +interface SelectionContext { + listDoubleSelection?: boolean; + listMultiSelection?: boolean; + webviewItems?: string; + webviewItemsValues?: GraphItemContext[]; +} + +interface SelectionContexts { + contexts: Map; + selectedShas: Set; +} + +const emptySelectionContext: SelectionContext = { + listDoubleSelection: false, + listMultiSelection: false, + webviewItems: undefined, + webviewItemsValues: undefined, +}; + +interface GraphWrapperAPI { + setRef: (refObject: GraphContainer) => void; +} + +const emptyRows: GraphRow[] = []; +// eslint-disable-next-line @typescript-eslint/naming-convention +export function GraphWrapperReact(props: Readonly) { + const [graph, _graphRef] = useState(null); + const [context, setContext] = useState(props.context); + const [selectionContexts, setSelectionContexts] = useState(); + + useEffect(() => { + setContext(props.context); + }, [props.context]); + + const graphRef = useCallback( + (graph: GraphContainer) => { + _graphRef(graph); + props.setRef(graph); + }, + [props.setRef], + ); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + const sha = getActiveRowInfo(props.activeRow)?.id; + if (sha == null) return; + + // TODO@eamodio a bit of a hack since the graph container ref isn't exposed in the types + const _graph = (graph as any)?.graphContainerRef.current; + if (!e.composedPath().some(el => el === _graph)) return; + + const row = props.rows?.find(r => r.sha === sha); + if (row == null) return; + + props.onDoubleClickRow?.({ row: row, preserveFocus: e.key !== 'Enter' }); + } + }; + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [props.activeRow]); + + const stopColumnResize = () => { + const activeResizeElement = document.querySelector('.graph-header .resizable.resizing'); + if (!activeResizeElement) return; + + // Trigger a mouseup event to reset the column resize state + document.dispatchEvent( + new MouseEvent('mouseup', { + view: window, + bubbles: true, + cancelable: true, + }), + ); + }; + + const handleOnGraphMouseLeave = (_event: React.MouseEvent) => { + props.onGraphMouseLeave?.(); + stopColumnResize(); + }; + + const handleMissingAvatars = (emails: GraphAvatars) => { + props.onMissingAvatars?.(emails); + }; + + const handleMissingRefsMetadata = (metadata: GraphMissingRefsMetadata) => { + props.onMissingRefsMetadata?.(metadata); + }; + + const handleToggleColumnSettings = (event: React.MouseEvent) => { + const e = event.nativeEvent; + const evt = new MouseEvent('contextmenu', { + bubbles: true, + clientX: e.clientX, + clientY: e.clientY, + }); + e.target?.dispatchEvent(evt); + e.stopImmediatePropagation(); + }; + + const handleMoreCommits = () => { + props.onMoreRows?.(); + }; + + const handleOnColumnResized = (columnName: GraphColumnName, columnSettings: GraphColumnSetting) => { + if (columnSettings.width) { + props.onChangeColumns?.({ + [columnName]: { + width: columnSettings.width, + isHidden: columnSettings.isHidden, + mode: columnSettings.mode as GraphColumnMode, + order: columnSettings.order, + }, + }); + } + }; + + const handleOnGraphVisibleRowsChanged = (top: GraphRow, bottom: GraphRow) => { + props.onChangeVisibleDays?.({ + top: new Date(top.date).setHours(23, 59, 59, 999), + bottom: new Date(bottom.date).setHours(0, 0, 0, 0), + }); + }; + + const handleOnGraphColumnsReOrdered = (columnsSettings: GraphColumnsSettings) => { + const graphColumnsConfig: GraphColumnsConfig = {}; + for (const [columnName, config] of Object.entries(columnsSettings as GraphColumnsConfig)) { + graphColumnsConfig[columnName] = { ...config }; + } + props.onChangeColumns?.(graphColumnsConfig); + }; + + // dirty trick to avoid mutations on the GraphContainer side + const fixedExcludeRefsById = useMemo( + (): ExcludeRefsById | undefined => (props.excludeRefs ? { ...props.excludeRefs } : undefined), + [props.excludeRefs], + ); + const handleOnToggleRefsVisibilityClick = (_event: any, refs: GraphRefOptData[], visible: boolean) => { + if (!visible) { + document.getElementById('hiddenRefs')?.animate( + [ + { offset: 0, background: 'transparent' }, + { + offset: 0.4, + background: 'var(--vscode-statusBarItem-warningBackground)', + }, + { offset: 1, background: 'transparent' }, + ], + { + duration: 1000, + iterations: !Object.keys(fixedExcludeRefsById ?? {}).length ? 2 : 1, + }, + ); + } + props.onChangeRefsVisibility?.({ refs: refs, visible: visible }); + }; + + const handleOnDoubleClickRef = ( + _event: React.MouseEvent, + refGroup: GraphRefGroup, + _row: GraphRow, + metadata?: GraphRefMetadataItem, + ) => { + if (refGroup.length > 0) { + props.onDoubleClickRef?.({ ref: refGroup[0], metadata: metadata }); + } + }; + + const handleOnDoubleClickRow = ( + _event: React.MouseEvent, + graphZoneType: GraphZoneType, + row: GraphRow, + ) => { + if (graphZoneType === refZone) return; + + props.onDoubleClickRow?.({ row: row, preserveFocus: true }); + }; + + const computeSelectionContext = (_active: GraphRow, rows: GraphRow[]) => { + if (rows.length <= 1) { + setSelectionContexts(undefined); + return; + } + + const selectedShas = new Set(); + for (const row of rows) { + selectedShas.add(row.sha); + } + + // Group the selected rows by their type and only include ones that have row context + const grouped = groupByFilterMap( + rows, + r => r.type, + r => + r.contexts?.row != null + ? ((typeof r.contexts.row === 'string' + ? JSON.parse(r.contexts.row) + : r.contexts.row) as GraphItemContext) + : undefined, + ); + + const contexts: SelectionContexts['contexts'] = new Map(); + + for (let [type, items] of grouped) { + let webviewItems: string | undefined; + + const contextValues = new Set(); + for (const item of items) { + contextValues.add(item.webviewItem); + } + + if (contextValues.size === 1) { + webviewItems = first(contextValues); + } else if (contextValues.size > 1) { + // If there are multiple contexts, see if they can be boiled down into a least common denominator set + // Contexts are of the form `gitlens:++...`, can also contain multiple `:`, but assume the whole thing is the type + + const itemTypes = new Map>(); + + for (const context of contextValues) { + const [type, ...adds] = context.split('+'); + + let additionalContext = itemTypes.get(type); + if (additionalContext == null) { + additionalContext ??= new Map(); + itemTypes.set(type, additionalContext); + } + + // If any item has no additional context, then only the type is able to be used + if (adds.length === 0) { + additionalContext.clear(); + break; + } + + for (const add of adds) { + additionalContext.set(add, (additionalContext.get(add) ?? 0) + 1); + } + } + + if (itemTypes.size === 1) { + let additionalContext; + [webviewItems, additionalContext] = first(itemTypes)!; + + if (additionalContext.size > 0) { + const commonContexts = join( + filterMap(additionalContext, ([context, count]) => + count === items.length ? context : undefined, + ), + '+', + ); + + if (commonContexts) { + webviewItems += `+${commonContexts}`; + } + } + } else { + // If we have more than one type, something is wrong with our context key setup -- should NOT happen at runtime + debugger; + webviewItems = undefined; + items = []; + } + } + + const count = items.length; + contexts.set(type, { + listDoubleSelection: count === 2, + listMultiSelection: count > 1, + webviewItems: webviewItems, + webviewItemsValues: count > 1 ? items : undefined, + }); + } + + setSelectionContexts({ contexts: contexts, selectedShas: selectedShas }); + }; + + const handleSelectGraphRows = (rows: GraphRow[]) => { + const active = rows[rows.length - 1]; + computeSelectionContext(active, rows); + + props.onChangeSelection?.(rows); + }; + + const handleRowContextMenu = (_event: React.MouseEvent, graphZoneType: GraphZoneType, graphRow: GraphRow) => { + if (graphZoneType === refZone) return; + props.onRowContextMenu?.({ graphZoneType: graphZoneType, graphRow: graphRow }); + // If the row is in the current selection, use the typed selection context, otherwise clear it + const newSelectionContext = selectionContexts?.selectedShas.has(graphRow.sha) + ? selectionContexts.contexts.get(graphRow.type) + : emptySelectionContext; + + setContext({ + ...context, + graph: { + ...(context?.graph != null && typeof context.graph === 'string' + ? JSON.parse(context.graph) + : context?.graph), + ...newSelectionContext, + }, + }); + }; + + return ( + } + cssVariables={props.theming?.cssVariables} + dimMergeCommits={props.config?.dimMergeCommits} + downstreamsByUpstream={props.downstreams} + enabledRefMetadataTypes={props.config?.enabledRefMetadataTypes} + enabledScrollMarkerTypes={props.config?.scrollMarkerTypes} + enableShowHideRefsOptions + enableMultiSelection={props.config?.enableMultiSelection} + excludeRefsById={props.excludeRefs} + excludeByType={props.excludeTypes} + formatCommitDateTime={getGraphDateFormatter(props.config)} + getExternalIcon={getIconElementLibrary} + graphRows={props.rows ?? emptyRows} + hasMoreCommits={props.paging?.hasMore} + // Just cast the { [id: string]: number } object to { [id: string]: boolean } for performance + highlightedShas={props.searchResults?.ids as GraphContainerProps['highlightedShas']} + highlightRowsOnRefHover={props.config?.highlightRowsOnRefHover} + includeOnlyRefsById={props.includeOnlyRefs} + scrollRowPadding={props.config?.scrollRowPadding} + showGhostRefsOnRowHover={props.config?.showGhostRefsOnRowHover} + showRemoteNamesOnRefs={props.config?.showRemoteNamesOnRefs} + isContainerWindowFocused={props.windowFocused} + isLoadingRows={props.loading} + isSelectedBySha={props.selectedRows} + nonce={props.nonce} + onColumnResized={handleOnColumnResized} + onDoubleClickGraphRow={handleOnDoubleClickRow} + onDoubleClickGraphRef={handleOnDoubleClickRef} + onGraphColumnsReOrdered={handleOnGraphColumnsReOrdered} + onGraphMouseLeave={handleOnGraphMouseLeave} + onGraphRowHovered={(e, graphZoneType, graphRow) => + props.onGraphRowHovered?.({ + clientX: e.clientX, + currentTarget: e.currentTarget, + graphRow: graphRow, + graphZoneType: graphZoneType, + }) + } + onGraphRowUnhovered={(e, graphZoneType, graphRow) => + props.onGraphRowUnhovered?.({ + relatedTarget: e.relatedTarget, + graphRow: graphRow, + graphZoneType: graphZoneType, + }) + } + onRowContextMenu={handleRowContextMenu} + onSettingsClick={handleToggleColumnSettings} + onSelectGraphRows={handleSelectGraphRows} + onToggleRefsVisibilityClick={handleOnToggleRefsVisibilityClick} + onEmailsMissingAvatarUrls={handleMissingAvatars} + onRefsMissingMetadata={handleMissingRefsMetadata} + onShowMoreCommits={handleMoreCommits} + onGraphVisibleRowsChanged={handleOnGraphVisibleRowsChanged} + platform={clientPlatform} + refMetadataById={props.refsMetadata} + rowsStats={props.rowsStats} + rowsStatsLoading={props.rowsStatsLoading} + searchMode={props.filter?.filter ? 'filter' : 'normal'} + shaLength={props.config?.idLength} + shiftSelectMode="simple" + suppressNonRefRowTooltips + themeOpacityFactor={props.theming?.themeOpacityFactor} + useAuthorInitialsForAvatars={!props.config?.avatars} + workDirStats={props.workingTreeStats} + /> + ); +} + +function formatCommitDateTime( + date: number, + style: DateStyle = 'absolute', + format: DateTimeFormat | string = 'short+short', + source?: CommitDateTimeSources, +): string { + switch (source) { + case CommitDateTimeSources.Tooltip: + return `${formatDate(date, format)} (${fromNow(date)})`; + case CommitDateTimeSources.RowEntry: + default: + return style === 'relative' ? fromNow(date) : formatDate(date, format); + } +} + +function getActiveRowInfo(activeRow: string | undefined): { id: string; date: number } | undefined { + if (activeRow == null) return undefined; + + const [id, date] = activeRow.split('|'); + return { + id: id, + date: Number(date), + }; +} diff --git a/src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.ts b/src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.ts new file mode 100644 index 0000000000000..a7b282efc3d3a --- /dev/null +++ b/src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.ts @@ -0,0 +1,316 @@ +import type GraphContainer from '@gitkraken/gitkraken-components'; +import type { GraphRef, GraphRow, GraphZoneType } from '@gitkraken/gitkraken-components'; +import { refZone } from '@gitkraken/gitkraken-components'; +import { consume } from '@lit/context'; +import { SignalWatcher } from '@lit-labs/signals'; +import r2wc from '@r2wc/react-to-web-component'; +import { html, LitElement } from 'lit'; +import { customElement, query } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import type { GitGraphRowType } from '../../../../../git/models/graph'; +import type { + GraphAvatars, + GraphColumnsConfig, + GraphExcludedRef, + GraphMissingRefsMetadata, + GraphRefMetadataItem, + UpdateGraphConfigurationParams, +} from '../../../../plus/graph/protocol'; +import { + DoubleClickedCommandType, + GetMissingAvatarsCommand, + GetMissingRefsMetadataCommand, + GetMoreRowsCommand, + GetRowHoverRequest, + UpdateColumnsCommand, + UpdateGraphConfigurationCommand, + UpdateRefsVisibilityCommand, + UpdateSelectionCommand, +} from '../../../../plus/graph/protocol'; +import type { CustomEventType } from '../../../shared/components/element'; +import type { TelemetryContext } from '../../../shared/context'; +import { ipcContext, telemetryContext } from '../../../shared/context'; +import { stateContext } from '../context'; +import '../hover/graphHover'; +import type { GlGraphHover } from '../hover/graphHover'; +import { graphStateContext } from '../stateProvider'; +import { GraphWrapperReact } from './graph-wrapper.react'; + +const WebGraph = r2wc(GraphWrapperReact, { + props: { + activeRow: 'string', + filter: 'json', + avatars: 'json', + columns: 'json', + context: 'json', + theming: 'json', + config: 'json', + downstreams: 'json', + excludeRefs: 'json', + excludeTypes: 'json', + rows: 'json', + includeOnlyRefs: 'json', + windowFocused: 'boolean', + loading: 'boolean', + selectedRows: 'json', + nonce: 'string', + refsMetadata: 'json', + rowsStats: 'json', + workingTreeStats: 'json', + paging: 'json', + setRef: 'function', + searchResults: 'json', + }, + + events: [ + 'onChangeColumns', + 'onGraphMouseLeave', + 'onChangeRefsVisibility', + 'onChangeSelection', + 'onDoubleClickRef', + 'onDoubleClickRow', + 'onMissingAvatars', + 'onMissingRefsMetadata', + 'onMoreRows', + 'onChangeVisibleDays', + 'onGraphRowHovered', + 'onGraphRowUnhovered', + 'onRowContextMenu', + ], +}); + +customElements.define('web-graph', WebGraph); + +declare global { + interface HTMLElementTagNameMap { + 'gl-graph-wrapper': GLGraphWrapper; + } + + interface GlobalEventHandlersEventMap { + // event map from react wrapped component + 'graph-changecolumns': CustomEvent<{ settings: GraphColumnsConfig }>; + 'graph-changegraphconfiguration': CustomEvent; + 'graph-changerefsvisibility': CustomEvent<{ refs: GraphExcludedRef[]; visible: boolean }>; + 'graph-changeselection': CustomEvent; + 'graph-doubleclickref': CustomEvent<{ ref: GraphRef; metadata?: GraphRefMetadataItem }>; + 'graph-doubleclickrow': CustomEvent<{ row: GraphRow; preserveFocus?: boolean }>; + 'graph-missingavatars': CustomEvent; + 'graph-missingrefsmetadata': CustomEvent; + 'graph-morerows': CustomEvent; + 'graph-changevisibledays': CustomEvent<{ top: number; bottom: number }>; + 'graph-graphrowhovered': CustomEvent<{ + clientX: number; + currentTarget: HTMLElement; + graphZoneType: GraphZoneType; + graphRow: GraphRow; + }>; + 'graph-graphrowunhovered': CustomEvent<{ + relatedTarget: HTMLElement; + graphZoneType: GraphZoneType; + graphRow: GraphRow; + }>; + 'graph-rowcontextmenu': CustomEvent; + 'graph-graphmouseleave': CustomEvent; + + // passing up event map + 'gl-graph-mouse-leave': CustomEvent; + 'gl-graph-change-visible-days': CustomEvent<{ top: number; bottom: number }>; + 'gl-graph-hovered-row': CustomEvent<{ graphZoneType: GraphZoneType; graphRow: GraphRow }>; + } +} + +@customElement('gl-graph-wrapper') +export class GLGraphWrapper extends SignalWatcher(LitElement) { + // use Light DOM + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this; + } + + @consume({ context: stateContext, subscribe: true }) + private readonly hostState!: typeof stateContext.__context__; + + @consume({ context: ipcContext }) + private readonly _ipc!: typeof ipcContext.__context__; + + @consume({ context: telemetryContext as any }) + private readonly _telemetry!: TelemetryContext; + + private onGetMissingAvatars({ detail: emails }: CustomEventType<'graph-missingavatars'>) { + this._ipc.sendCommand(GetMissingAvatarsCommand, { emails: emails }); + } + + private onGetMissingRefsMetadata({ detail: metadata }: CustomEventType<'graph-missingrefsmetadata'>) { + this._ipc.sendCommand(GetMissingRefsMetadataCommand, { metadata: metadata }); + } + + private onGetMoreRows({ detail: sha }: CustomEventType<'graph-morerows'>) { + this.graphAppState.loading = true; + this._ipc.sendCommand(GetMoreRowsCommand, { id: sha }); + } + + private onColumnsChanged(event: CustomEventType<'graph-changecolumns'>) { + this._ipc.sendCommand(UpdateColumnsCommand, { + config: event.detail.settings, + }); + } + + private onRefsVisibilityChanged({ detail }: CustomEventType<'graph-changerefsvisibility'>) { + this._ipc.sendCommand(UpdateRefsVisibilityCommand, detail); + } + + private onDoubleClickRef({ detail: { ref, metadata } }: CustomEventType<'graph-doubleclickref'>) { + this._ipc.sendCommand(DoubleClickedCommandType, { + type: 'ref', + ref: ref, + metadata: metadata, + }); + } + + private onDoubleClickRow({ detail: { row, preserveFocus } }: CustomEventType<'graph-doubleclickrow'>) { + this._ipc.sendCommand(DoubleClickedCommandType, { + type: 'row', + row: { id: row.sha, type: row.type as GitGraphRowType }, + preserveFocus: preserveFocus, + }); + } + + private onGraphConfigurationChanged({ detail: changes }: CustomEventType<'graph-changegraphconfiguration'>) { + this._ipc.sendCommand(UpdateGraphConfigurationCommand, { changes: changes }); + } + + private onSelectionChanged({ detail: rows }: CustomEventType<'graph-changeselection'>) { + const selection = rows.filter(r => r != null).map(r => ({ id: r.sha, type: r.type as GitGraphRowType })); + this._telemetry.sendEvent({ name: 'graph/row/selected', data: { rows: selection.length } }); + + this.graphHover.hide(); + + const active = rows[rows.length - 1]; + const activeKey = active != null ? `${active.sha}|${active.date}` : undefined; + this.graphAppState.activeRow = activeKey; + this.graphAppState.activeDay = active?.date; + + this._ipc.sendCommand(UpdateSelectionCommand, { + selection: selection, + }); + } + + private async onHoverRowPromise(row: GraphRow) { + try { + const request = await this._ipc.sendRequest(GetRowHoverRequest, { + type: row.type as GitGraphRowType, + id: row.sha, + }); + this._telemetry.sendEvent({ name: 'graph/row/hovered', data: {} }); + return request; + } catch (ex) { + return { id: row.sha, markdown: { status: 'rejected' as const, reason: ex } }; + } + } + + private handleOnGraphRowHovered({ + detail: { graphRow, graphZoneType, clientX, currentTarget }, + }: CustomEventType<'graph-graphrowhovered'>) { + if (graphZoneType === refZone) return; + this.dispatchEvent( + new CustomEvent('gl-graph-hovered-row', { detail: { graphZoneType: graphZoneType, graphRow: graphRow } }), + ); + const hoverComponent = this.graphHover; + if (hoverComponent == null) return; + const rect = currentTarget.getBoundingClientRect(); + const x = clientX; + const y = rect.top; + const height = rect.height; + const width = 60; // Add some width, so `skidding` will be able to apply + const anchor = { + getBoundingClientRect: function () { + return { + width: width, + height: height, + x: x, + y: y, + top: y, + left: x, + right: x + width, + bottom: y + height, + }; + }, + }; + hoverComponent.requestMarkdown ??= this.onHoverRowPromise.bind(this); + hoverComponent.onRowHovered(graphRow, anchor); + } + + private handleOnGraphRowUnhovered({ + detail: { graphRow, graphZoneType, relatedTarget }, + }: CustomEventType<'graph-graphrowunhovered'>) { + if (graphZoneType === refZone) return; + this.graphHover.onRowUnhovered(graphRow, relatedTarget); + } + + @query('web-graph') + webGraph!: typeof WebGraph; + + selectCommits(shaList: string[], includeToPrevSel: boolean, isAutoOrKeyScroll: boolean) { + this.ref?.selectCommits(shaList, includeToPrevSel, isAutoOrKeyScroll); + } + + private onChangeVisibleDays({ detail }: CustomEventType<'graph-changevisibledays'>) { + this.dispatchEvent(new CustomEvent('gl-graph-change-visible-days', { detail: detail })); + } + + @consume({ context: graphStateContext }) + private readonly graphAppState!: typeof graphStateContext.__context__; + + private ref?: GraphContainer; + + @query('gl-graph-hover#commit-hover') + private readonly graphHover!: GlGraphHover; + + private handleRowContextMenu() { + this.graphHover.hide(); + } + + override render() { + return html` { + this.ref = ref; + }} + .filter=${{ ...this.graphAppState.filter }} + @changecolumns=${this.onColumnsChanged} + @changegraphconfiguration=${this.onGraphConfigurationChanged} + @changerefsvisibility=${this.onRefsVisibilityChanged} + @changeselection=${this.onSelectionChanged} + @doubleclickref=${this.onDoubleClickRef} + @doubleclickrow=${this.onDoubleClickRow} + @missingavatars=${this.onGetMissingAvatars} + @missingrefsmetadata=${this.onGetMissingRefsMetadata} + @morerows=${this.onGetMoreRows} + @changevisibledays=${this.onChangeVisibleDays} + @graphrowhovered=${this.handleOnGraphRowHovered} + @graphrowunhovered=${this.handleOnGraphRowUnhovered} + @rowcontextmenu=${this.handleRowContextMenu} + @graphmouseleave=${(e: CustomEvent) => + this.dispatchEvent(new CustomEvent('gl-graph-mouse-leave', { detail: e.detail }))} + >`; + } +} diff --git a/src/webviews/apps/plus/graph/graph.html b/src/webviews/apps/plus/graph/graph.html index f66fe86c163e7..8d2d88c3b48f0 100644 --- a/src/webviews/apps/plus/graph/graph.html +++ b/src/webviews/apps/plus/graph/graph.html @@ -24,7 +24,12 @@ data-placement="#{placement}" data-vscode-context='{ "preventDefaultContextMenuItems": true, "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }' > -
+ #{endOfBody} diff --git a/src/webviews/apps/plus/graph/graph.scss b/src/webviews/apps/plus/graph/graph.scss index 63f74cdc00798..f0751f0801e8c 100644 --- a/src/webviews/apps/plus/graph/graph.scss +++ b/src/webviews/apps/plus/graph/graph.scss @@ -339,14 +339,9 @@ button:not([disabled]), } &__indicator { - position: absolute; - bottom: 0.2rem; - right: 1.5rem; - display: block; - width: 0.8rem; - height: 0.8rem; - border-radius: 100%; - background-color: var(--vscode-progressBar-background); + margin-left: -0.2rem; + --gl-indicator-color: green; + --gl-indicator-size: 0.4rem; } &__small { @@ -936,6 +931,18 @@ gl-feature-gate gl-feature-badge { margin-right: 0.4rem; } +gl-graph-app-wc { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +gl-graph-wrapper, +web-graph { + display: contents; +} + .graph-app { --fs-1: 1.1rem; --fs-2: 1.3rem; diff --git a/src/webviews/apps/plus/graph/graph.ts b/src/webviews/apps/plus/graph/graph.ts new file mode 100644 index 0000000000000..03564edb74c63 --- /dev/null +++ b/src/webviews/apps/plus/graph/graph.ts @@ -0,0 +1,289 @@ +/*global document window*/ +import type { CssVariables } from '@gitkraken/gitkraken-components'; +import { provide } from '@lit/context'; +import '@shoelace-style/shoelace/dist/components/option/option.component.js'; +import '@shoelace-style/shoelace/dist/components/select/select.component.js'; +import { html } from 'lit'; +import { customElement, query, state } from 'lit/decorators.js'; +import { Color, getCssVariable, mix, opacity } from '../../../../system/color'; +import type { State } from '../../../plus/graph/protocol'; +import type { StateProvider } from '../../shared/app'; +import { GlApp } from '../../shared/app'; +import '../../shared/components/branch-icon'; +import '../../shared/components/button'; +import '../../shared/components/code-icon'; +import '../../shared/components/menu'; +import '../../shared/components/overlays/popover'; +import '../../shared/components/overlays/tooltip'; +import '../../shared/components/rich/issue-pull-request'; +import type { HostIpc } from '../../shared/ipc'; +import type { ThemeChangeEvent } from '../../shared/theme'; +import '../shared/components/merge-rebase-status'; +import './actions/gitActionsButtons.wc'; +import './graph-app'; +import type { GraphAppWC } from './graph-app'; +import './graph-header'; +import './graph-wrapper/graph-wrapper'; +import './graph.scss'; +import './minimap/minimap-container'; +import './sidebar/sidebar'; +import { GraphAppState, graphStateContext, GraphStateProvider } from './stateProvider'; + +const graphLaneThemeColors = new Map([ + ['--vscode-gitlens-graphLane1Color', '#15a0bf'], + ['--vscode-gitlens-graphLane2Color', '#0669f7'], + ['--vscode-gitlens-graphLane3Color', '#8e00c2'], + ['--vscode-gitlens-graphLane4Color', '#c517b6'], + ['--vscode-gitlens-graphLane5Color', '#d90171'], + ['--vscode-gitlens-graphLane6Color', '#cd0101'], + ['--vscode-gitlens-graphLane7Color', '#f25d2e'], + ['--vscode-gitlens-graphLane8Color', '#f2ca33'], + ['--vscode-gitlens-graphLane9Color', '#7bd938'], + ['--vscode-gitlens-graphLane10Color', '#2ece9d'], +]); + +@customElement('gl-graph-app') +export class GraphApp extends GlApp { + @state() + searching: string = ''; + searchResultsHidden: unknown; + get hasFilters() { + if (this.state.config?.onlyFollowFirstParent) return true; + if (this.state.excludeTypes == null) return false; + + return Object.values(this.state.excludeTypes).includes(true); + } + private applyTheme(theme: { cssVariables: CssVariables; themeOpacityFactor: number }) { + this._graphState.theming = theme; + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this; + } + + @provide({ context: graphStateContext }) + private readonly _graphState: typeof graphStateContext.__context__ = new GraphAppState(); + + protected override createStateProvider(state: State, ipc: HostIpc): StateProvider { + return new GraphStateProvider(this, state, ipc, this._logger, { + onStateUpdate: partial => { + if ('loading' in partial) { + this._graphState.loading = partial.loading ?? false; + } + if ('rows' in partial) { + this.appElement.resetHover(); + } + if ('selectedRows' in partial) { + this._graphState.selectedRows = partial.selectedRows; + } + if ('searchResults' in partial) { + this._graphState.searchResultsResponse = partial.searchResults; + } + }, + }); + } + + private getGraphTheming(): { cssVariables: CssVariables; themeOpacityFactor: number } { + // this will be called on theme updated as well as on config updated since it is dependent on the column colors from config changes and the background color from the theme + const computedStyle = window.getComputedStyle(document.documentElement); + const bgColor = getCssVariable('--color-background', computedStyle); + + const mixedGraphColors: CssVariables = {}; + + let i = 0; + let color; + for (const [colorVar, colorDefault] of graphLaneThemeColors) { + color = getCssVariable(colorVar, computedStyle) || colorDefault; + + mixedGraphColors[`--column-${i}-color`] = color; + + mixedGraphColors[`--graph-color-${i}`] = color; + for (const mixInt of [15, 25, 45, 50]) { + mixedGraphColors[`--graph-color-${i}-bg${mixInt}`] = mix(bgColor, color, mixInt); + } + for (const mixInt of [10, 50]) { + mixedGraphColors[`--graph-color-${i}-f${mixInt}`] = opacity(color, mixInt); + } + + i++; + } + + const isHighContrastTheme = + document.body.classList.contains('vscode-high-contrast') || + document.body.classList.contains('vscode-high-contrast-light'); + + return { + cssVariables: { + '--app__bg0': bgColor, + '--panel__bg0': getCssVariable('--color-graph-background', computedStyle), + '--panel__bg1': getCssVariable('--color-graph-background2', computedStyle), + '--section-border': getCssVariable('--color-graph-background2', computedStyle), + + '--selected-row': getCssVariable('--color-graph-selected-row', computedStyle), + '--selected-row-border': isHighContrastTheme + ? `1px solid ${getCssVariable('--color-graph-contrast-border', computedStyle)}` + : 'none', + '--hover-row': getCssVariable('--color-graph-hover-row', computedStyle), + '--hover-row-border': isHighContrastTheme + ? `1px dashed ${getCssVariable('--color-graph-contrast-border', computedStyle)}` + : 'none', + + '--scrollable-scrollbar-thickness': getCssVariable('--graph-column-scrollbar-thickness', computedStyle), + '--scroll-thumb-bg': getCssVariable('--vscode-scrollbarSlider-background', computedStyle), + + '--scroll-marker-head-color': getCssVariable('--color-graph-scroll-marker-head', computedStyle), + '--scroll-marker-upstream-color': getCssVariable('--color-graph-scroll-marker-upstream', computedStyle), + '--scroll-marker-highlights-color': getCssVariable( + '--color-graph-scroll-marker-highlights', + computedStyle, + ), + '--scroll-marker-local-branches-color': getCssVariable( + '--color-graph-scroll-marker-local-branches', + computedStyle, + ), + '--scroll-marker-remote-branches-color': getCssVariable( + '--color-graph-scroll-marker-remote-branches', + computedStyle, + ), + '--scroll-marker-stashes-color': getCssVariable('--color-graph-scroll-marker-stashes', computedStyle), + '--scroll-marker-tags-color': getCssVariable('--color-graph-scroll-marker-tags', computedStyle), + '--scroll-marker-selection-color': getCssVariable( + '--color-graph-scroll-marker-selection', + computedStyle, + ), + '--scroll-marker-pull-requests-color': getCssVariable( + '--color-graph-scroll-marker-pull-requests', + computedStyle, + ), + + '--stats-added-color': getCssVariable('--color-graph-stats-added', computedStyle), + '--stats-deleted-color': getCssVariable('--color-graph-stats-deleted', computedStyle), + '--stats-files-color': getCssVariable('--color-graph-stats-files', computedStyle), + '--stats-bar-border-radius': getCssVariable('--graph-stats-bar-border-radius', computedStyle), + '--stats-bar-height': getCssVariable('--graph-stats-bar-height', computedStyle), + + '--text-selected': getCssVariable('--color-graph-text-selected', computedStyle), + '--text-selected-row': getCssVariable('--color-graph-text-selected-row', computedStyle), + '--text-hovered': getCssVariable('--color-graph-text-hovered', computedStyle), + '--text-dimmed-selected': getCssVariable('--color-graph-text-dimmed-selected', computedStyle), + '--text-dimmed': getCssVariable('--color-graph-text-dimmed', computedStyle), + '--text-normal': getCssVariable('--color-graph-text-normal', computedStyle), + '--text-secondary': getCssVariable('--color-graph-text-secondary', computedStyle), + '--text-disabled': getCssVariable('--color-graph-text-disabled', computedStyle), + + '--text-accent': getCssVariable('--color-link-foreground', computedStyle), + '--text-inverse': getCssVariable('--vscode-input-background', computedStyle), + '--text-bright': getCssVariable('--vscode-input-background', computedStyle), + ...mixedGraphColors, + }, + themeOpacityFactor: parseInt(getCssVariable('--graph-theme-opacity-factor', computedStyle)) || 1, + }; + } + + protected override onThemeUpdated(e: ThemeChangeEvent) { + const rootStyle = document.documentElement.style; + + const backgroundColor = Color.from(e.colors.background); + const foregroundColor = Color.from(e.colors.foreground); + + const backgroundLuminance = backgroundColor.getRelativeLuminance(); + const foregroundLuminance = foregroundColor.getRelativeLuminance(); + + const themeLuminance = (luminance: number) => { + let min; + let max; + if (foregroundLuminance > backgroundLuminance) { + max = foregroundLuminance; + min = backgroundLuminance; + } else { + min = foregroundLuminance; + max = backgroundLuminance; + } + const percent = luminance / 1; + return percent * (max - min) + min; + }; + + // minimap and scroll markers + + let c = Color.fromCssVariable('--vscode-scrollbarSlider-background', e.computedStyle); + rootStyle.setProperty( + '--color-graph-minimap-visibleAreaBackground', + c.luminance(themeLuminance(e.isLightTheme ? 0.6 : 0.1)).toString(), + ); + + if (!e.isLightTheme) { + c = Color.fromCssVariable('--color-graph-scroll-marker-local-branches', e.computedStyle); + rootStyle.setProperty( + '--color-graph-minimap-tip-branchBackground', + c.luminance(themeLuminance(0.55)).toString(), + ); + + c = Color.fromCssVariable('--color-graph-scroll-marker-local-branches', e.computedStyle); + rootStyle.setProperty( + '--color-graph-minimap-tip-branchBorder', + c.luminance(themeLuminance(0.55)).toString(), + ); + + c = Color.fromCssVariable('--vscode-editor-foreground', e.computedStyle); + const tipForeground = c.isLighter() ? c.luminance(0.01).toString() : c.luminance(0.99).toString(); + rootStyle.setProperty('--color-graph-minimap-tip-headForeground', tipForeground); + rootStyle.setProperty('--color-graph-minimap-tip-upstreamForeground', tipForeground); + rootStyle.setProperty('--color-graph-minimap-tip-highlightForeground', tipForeground); + rootStyle.setProperty('--color-graph-minimap-tip-branchForeground', tipForeground); + } + + const branchStatusLuminance = themeLuminance(e.isLightTheme ? 0.72 : 0.064); + const branchStatusHoverLuminance = themeLuminance(e.isLightTheme ? 0.64 : 0.076); + const branchStatusPillLuminance = themeLuminance(e.isLightTheme ? 0.92 : 0.02); + // branch status ahead + c = Color.fromCssVariable('--branch-status-ahead-foreground', e.computedStyle); + rootStyle.setProperty('--branch-status-ahead-background', c.luminance(branchStatusLuminance).toString()); + rootStyle.setProperty( + '--branch-status-ahead-hover-background', + c.luminance(branchStatusHoverLuminance).toString(), + ); + rootStyle.setProperty( + '--branch-status-ahead-pill-background', + c.luminance(branchStatusPillLuminance).toString(), + ); + + // branch status behind + c = Color.fromCssVariable('--branch-status-behind-foreground', e.computedStyle); + rootStyle.setProperty('--branch-status-behind-background', c.luminance(branchStatusLuminance).toString()); + rootStyle.setProperty( + '--branch-status-behind-hover-background', + c.luminance(branchStatusHoverLuminance).toString(), + ); + rootStyle.setProperty( + '--branch-status-behind-pill-background', + c.luminance(branchStatusPillLuminance).toString(), + ); + + // branch status both + c = Color.fromCssVariable('--branch-status-both-foreground', e.computedStyle); + rootStyle.setProperty('--branch-status-both-background', c.luminance(branchStatusLuminance).toString()); + rootStyle.setProperty( + '--branch-status-both-hover-background', + c.luminance(branchStatusHoverLuminance).toString(), + ); + rootStyle.setProperty( + '--branch-status-both-pill-background', + c.luminance(branchStatusPillLuminance).toString(), + ); + + const th = this.getGraphTheming(); + Object.entries(th.cssVariables).forEach(([property, value]) => { + rootStyle.setProperty(property, value.toString()); + }); + this.applyTheme(th); + } + + @query('gl-graph-app-wc') + private appElement!: GraphAppWC; + + override render() { + return html``; + } +} + +new GraphApp(); diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx deleted file mode 100644 index 4a51bf0498170..0000000000000 --- a/src/webviews/apps/plus/graph/graph.tsx +++ /dev/null @@ -1,697 +0,0 @@ -/*global document window*/ -import type { CssVariables, GraphRef, GraphRefOptData, GraphRow } from '@gitkraken/gitkraken-components'; -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import type { GraphBranchesVisibility } from '../../../../config'; -import type { SearchQuery } from '../../../../constants.search'; -import type { GitGraphRowType } from '../../../../git/models/graph'; -import { Color, getCssVariable, mix, opacity } from '../../../../system/color'; -import { debug, log } from '../../../../system/decorators/log'; -import { debounce } from '../../../../system/function'; -import { getLogScope, setLogScopeExit } from '../../../../system/logger.scope'; -import type { - DidSearchParams, - GraphAvatars, - GraphColumnsConfig, - GraphExcludedRef, - GraphExcludeTypes, - GraphMissingRefsMetadata, - GraphRefMetadataItem, - GraphSearchMode, - InternalNotificationType, - State, - UpdateGraphConfigurationParams, - UpdateStateCallback, -} from '../../../plus/graph/protocol'; -import { - ChooseRefRequest, - ChooseRepositoryCommand, - DidChangeAvatarsNotification, - DidChangeBranchStateNotification, - DidChangeColumnsNotification, - DidChangeGraphConfigurationNotification, - DidChangeNotification, - DidChangeRefsMetadataNotification, - DidChangeRefsVisibilityNotification, - DidChangeRepoConnectionNotification, - DidChangeRowsNotification, - DidChangeRowsStatsNotification, - DidChangeScrollMarkersNotification, - DidChangeSelectionNotification, - DidChangeSubscriptionNotification, - DidChangeWorkingTreeNotification, - DidFetchNotification, - DidSearchNotification, - DidStartFeaturePreviewNotification, - DoubleClickedCommandType, - EnsureRowRequest, - GetMissingAvatarsCommand, - GetMissingRefsMetadataCommand, - GetMoreRowsCommand, - GetRowHoverRequest, - OpenPullRequestDetailsCommand, - SearchOpenInViewCommand, - SearchRequest, - UpdateColumnsCommand, - UpdateExcludeTypesCommand, - UpdateGraphConfigurationCommand, - UpdateGraphSearchModeCommand, - UpdateIncludedRefsCommand, - UpdateRefsVisibilityCommand, - UpdateSelectionCommand, -} from '../../../plus/graph/protocol'; -import type { IpcMessage, IpcNotification } from '../../../protocol'; -import { DidChangeHostWindowFocusNotification } from '../../../protocol'; -import { App } from '../../shared/appBase'; -import type { Disposable } from '../../shared/events'; -import type { ThemeChangeEvent } from '../../shared/theme'; -import { GraphWrapper } from './GraphWrapper'; -import './graph.scss'; - -const graphLaneThemeColors = new Map([ - ['--vscode-gitlens-graphLane1Color', '#15a0bf'], - ['--vscode-gitlens-graphLane2Color', '#0669f7'], - ['--vscode-gitlens-graphLane3Color', '#8e00c2'], - ['--vscode-gitlens-graphLane4Color', '#c517b6'], - ['--vscode-gitlens-graphLane5Color', '#d90171'], - ['--vscode-gitlens-graphLane6Color', '#cd0101'], - ['--vscode-gitlens-graphLane7Color', '#f25d2e'], - ['--vscode-gitlens-graphLane8Color', '#f2ca33'], - ['--vscode-gitlens-graphLane9Color', '#7bd938'], - ['--vscode-gitlens-graphLane10Color', '#2ece9d'], -]); - -export class GraphApp extends App { - private updateStateCallback?: UpdateStateCallback; - - constructor() { - super('GraphApp'); - } - - @log() - protected override onBind(): Disposable[] { - const disposables = super.onBind?.() ?? []; - // disposables.push(DOM.on(window, 'keyup', e => this.onKeyUp(e))); - - this.ensureTheming(this.state); - - const $rootElement = document.getElementById('root'); - const root = createRoot($rootElement!); - if ($rootElement != null) { - root.render( - this.registerUpdateStateCallback(updateState)} - onChangeColumns={debounce( - settings => this.onColumnsChanged(settings), - 250, - )} - onChangeExcludeTypes={this.onExcludeTypesChanged.bind(this)} - onChangeGraphConfiguration={this.onGraphConfigurationChanged.bind(this)} - onChangeGraphSearchMode={this.onGraphSearchModeChanged.bind(this)} - onChangeRefIncludes={this.onRefIncludesChanged.bind(this)} - onChangeRefsVisibility={(refs: GraphExcludedRef[], visible: boolean) => - this.onRefsVisibilityChanged(refs, visible) - } - onChangeSelection={debounce( - rows => this.onSelectionChanged(rows), - 250, - )} - onChooseRepository={debounce(() => this.onChooseRepository(), 250)} - onDoubleClickRef={(ref, metadata) => this.onDoubleClickRef(ref, metadata)} - onDoubleClickRow={(row, preserveFocus) => this.onDoubleClickRow(row, preserveFocus)} - onEnsureRowPromise={this.onEnsureRowPromise.bind(this)} - onHoverRowPromise={(row: GraphRow) => this.onHoverRowPromise(row)} - onJumpToRefPromise={(shift: boolean) => this.onJumpToRefPromise(shift)} - onMissingAvatars={(...params) => this.onGetMissingAvatars(...params)} - onMissingRefsMetadata={(...params) => this.onGetMissingRefsMetadata(...params)} - onMoreRows={(...params) => this.onGetMoreRows(...params)} - onOpenPullRequest={(...params) => this.onOpenPullRequest(...params)} - onSearch={debounce((search, options) => this.onSearch(search, options), 250)} - onSearchPromise={(...params) => this.onSearchPromise(...params)} - onSearchOpenInView={(...params) => this.onSearchOpenInView(...params)} - />, - ); - disposables.push({ - dispose: () => { - root.unmount(); - }, - }); - } - - return disposables; - } - - // private onKeyUp(e: KeyboardEvent) { - // if (e.key === 'Enter' || e.key === ' ') { - // const inputFocused = e.composedPath().some(el => (el as HTMLElement).tagName === 'INPUT'); - // if (!inputFocused) return; - - // const $target = e.target as HTMLElement; - // } - // } - - protected override onMessageReceived(msg: IpcMessage): void { - const scope = getLogScope(); - - switch (true) { - case DidChangeNotification.is(msg): - this.setState({ ...this.state, ...msg.params.state }, DidChangeNotification); - break; - - case DidFetchNotification.is(msg): - this.state.lastFetched = msg.params.lastFetched; - this.setState(this.state, DidFetchNotification); - break; - - case DidChangeAvatarsNotification.is(msg): - this.state.avatars = msg.params.avatars; - this.setState(this.state, DidChangeAvatarsNotification); - break; - case DidStartFeaturePreviewNotification.is(msg): - this.state.featurePreview = msg.params.featurePreview; - this.state.allowed = msg.params.allowed; - this.setState(this.state, DidStartFeaturePreviewNotification); - break; - case DidChangeBranchStateNotification.is(msg): - this.state.branchState = msg.params.branchState; - this.setState(this.state, DidChangeBranchStateNotification); - break; - - case DidChangeHostWindowFocusNotification.is(msg): - this.state.windowFocused = msg.params.focused; - this.setState(this.state, DidChangeHostWindowFocusNotification); - break; - - case DidChangeColumnsNotification.is(msg): - this.state.columns = msg.params.columns; - this.state.context = { - ...this.state.context, - header: msg.params.context, - settings: msg.params.settingsContext, - }; - this.setState(this.state, DidChangeColumnsNotification); - break; - - case DidChangeRefsVisibilityNotification.is(msg): - this.state.branchesVisibility = msg.params.branchesVisibility; - this.state.excludeRefs = msg.params.excludeRefs; - this.state.excludeTypes = msg.params.excludeTypes; - this.state.includeOnlyRefs = msg.params.includeOnlyRefs; - this.setState(this.state, DidChangeRefsVisibilityNotification); - break; - - case DidChangeRefsMetadataNotification.is(msg): - this.state.refsMetadata = msg.params.metadata; - this.setState(this.state, DidChangeRefsMetadataNotification); - break; - - case DidChangeRowsNotification.is(msg): { - let rows; - if (msg.params.rows.length && msg.params.paging?.startingCursor != null && this.state.rows != null) { - const previousRows = this.state.rows; - const lastId = previousRows[previousRows.length - 1]?.sha; - - let previousRowsLength = previousRows.length; - const newRowsLength = msg.params.rows.length; - - this.log( - scope, - `paging in ${newRowsLength} rows into existing ${previousRowsLength} rows at ${msg.params.paging.startingCursor} (last existing row: ${lastId})`, - ); - - rows = []; - // Preallocate the array to avoid reallocations - rows.length = previousRowsLength + newRowsLength; - - if (msg.params.paging.startingCursor !== lastId) { - this.log(scope, `searching for ${msg.params.paging.startingCursor} in existing rows`); - - let i = 0; - let row; - for (row of previousRows) { - rows[i++] = row; - if (row.sha === msg.params.paging.startingCursor) { - this.log(scope, `found ${msg.params.paging.startingCursor} in existing rows`); - - previousRowsLength = i; - - if (previousRowsLength !== previousRows.length) { - // If we stopped before the end of the array, we need to trim it - rows.length = previousRowsLength + newRowsLength; - } - - break; - } - } - } else { - for (let i = 0; i < previousRowsLength; i++) { - rows[i] = previousRows[i]; - } - } - - for (let i = 0; i < newRowsLength; i++) { - rows[previousRowsLength + i] = msg.params.rows[i]; - } - } else { - this.log(scope, `setting to ${msg.params.rows.length} rows`); - - if (msg.params.rows.length === 0) { - rows = this.state.rows; - } else { - rows = msg.params.rows; - } - } - - this.state.avatars = msg.params.avatars; - this.state.downstreams = msg.params.downstreams; - if (msg.params.refsMetadata !== undefined) { - this.state.refsMetadata = msg.params.refsMetadata; - } - this.state.rows = rows; - this.state.paging = msg.params.paging; - if (msg.params.rowsStats != null) { - this.state.rowsStats = { ...this.state.rowsStats, ...msg.params.rowsStats }; - } - this.state.rowsStatsLoading = msg.params.rowsStatsLoading; - if (msg.params.selectedRows != null) { - this.state.selectedRows = msg.params.selectedRows; - } - this.state.loading = false; - this.setState(this.state, DidChangeRowsNotification); - - setLogScopeExit(scope, ` \u2022 rows=${this.state.rows?.length ?? 0}`); - break; - } - case DidChangeRowsStatsNotification.is(msg): - this.state.rowsStats = { ...this.state.rowsStats, ...msg.params.rowsStats }; - this.state.rowsStatsLoading = msg.params.rowsStatsLoading; - this.setState(this.state, DidChangeRowsStatsNotification); - break; - - case DidChangeScrollMarkersNotification.is(msg): - this.state.context = { ...this.state.context, settings: msg.params.context }; - this.setState(this.state, DidChangeScrollMarkersNotification); - break; - - case DidSearchNotification.is(msg): - this.updateSearchResultState(msg.params); - break; - - case DidChangeSelectionNotification.is(msg): - this.state.selectedRows = msg.params.selection; - this.setState(this.state, DidChangeSelectionNotification); - break; - - case DidChangeGraphConfigurationNotification.is(msg): - this.state.config = msg.params.config; - this.setState(this.state, DidChangeGraphConfigurationNotification); - break; - - case DidChangeSubscriptionNotification.is(msg): - this.state.subscription = msg.params.subscription; - this.state.allowed = msg.params.allowed; - this.setState(this.state, DidChangeSubscriptionNotification); - break; - - case DidChangeWorkingTreeNotification.is(msg): - this.state.workingTreeStats = msg.params.stats; - this.setState(this.state, DidChangeWorkingTreeNotification); - break; - - case DidChangeRepoConnectionNotification.is(msg): - this.state.repositories = msg.params.repositories; - this.setState(this.state, DidChangeRepoConnectionNotification); - break; - - default: - super.onMessageReceived?.(msg); - } - } - - protected override onThemeUpdated(e: ThemeChangeEvent): void { - const rootStyle = document.documentElement.style; - - const backgroundColor = Color.from(e.colors.background); - const foregroundColor = Color.from(e.colors.foreground); - - const backgroundLuminance = backgroundColor.getRelativeLuminance(); - const foregroundLuminance = foregroundColor.getRelativeLuminance(); - - const themeLuminance = (luminance: number) => { - let min; - let max; - if (foregroundLuminance > backgroundLuminance) { - max = foregroundLuminance; - min = backgroundLuminance; - } else { - min = foregroundLuminance; - max = backgroundLuminance; - } - const percent = luminance / 1; - return percent * (max - min) + min; - }; - - // minimap and scroll markers - - let c = Color.fromCssVariable('--vscode-scrollbarSlider-background', e.computedStyle); - rootStyle.setProperty( - '--color-graph-minimap-visibleAreaBackground', - c.luminance(themeLuminance(e.isLightTheme ? 0.6 : 0.1)).toString(), - ); - - if (!e.isLightTheme) { - c = Color.fromCssVariable('--color-graph-scroll-marker-local-branches', e.computedStyle); - rootStyle.setProperty( - '--color-graph-minimap-tip-branchBackground', - c.luminance(themeLuminance(0.55)).toString(), - ); - - c = Color.fromCssVariable('--color-graph-scroll-marker-local-branches', e.computedStyle); - rootStyle.setProperty( - '--color-graph-minimap-tip-branchBorder', - c.luminance(themeLuminance(0.55)).toString(), - ); - - c = Color.fromCssVariable('--vscode-editor-foreground', e.computedStyle); - const tipForeground = c.isLighter() ? c.luminance(0.01).toString() : c.luminance(0.99).toString(); - rootStyle.setProperty('--color-graph-minimap-tip-headForeground', tipForeground); - rootStyle.setProperty('--color-graph-minimap-tip-upstreamForeground', tipForeground); - rootStyle.setProperty('--color-graph-minimap-tip-highlightForeground', tipForeground); - rootStyle.setProperty('--color-graph-minimap-tip-branchForeground', tipForeground); - } - - const branchStatusLuminance = themeLuminance(e.isLightTheme ? 0.72 : 0.064); - const branchStatusHoverLuminance = themeLuminance(e.isLightTheme ? 0.64 : 0.076); - const branchStatusPillLuminance = themeLuminance(e.isLightTheme ? 0.92 : 0.02); - // branch status ahead - c = Color.fromCssVariable('--branch-status-ahead-foreground', e.computedStyle); - rootStyle.setProperty('--branch-status-ahead-background', c.luminance(branchStatusLuminance).toString()); - rootStyle.setProperty( - '--branch-status-ahead-hover-background', - c.luminance(branchStatusHoverLuminance).toString(), - ); - rootStyle.setProperty( - '--branch-status-ahead-pill-background', - c.luminance(branchStatusPillLuminance).toString(), - ); - - // branch status behind - c = Color.fromCssVariable('--branch-status-behind-foreground', e.computedStyle); - rootStyle.setProperty('--branch-status-behind-background', c.luminance(branchStatusLuminance).toString()); - rootStyle.setProperty( - '--branch-status-behind-hover-background', - c.luminance(branchStatusHoverLuminance).toString(), - ); - rootStyle.setProperty( - '--branch-status-behind-pill-background', - c.luminance(branchStatusPillLuminance).toString(), - ); - - // branch status both - c = Color.fromCssVariable('--branch-status-both-foreground', e.computedStyle); - rootStyle.setProperty('--branch-status-both-background', c.luminance(branchStatusLuminance).toString()); - rootStyle.setProperty( - '--branch-status-both-hover-background', - c.luminance(branchStatusHoverLuminance).toString(), - ); - rootStyle.setProperty( - '--branch-status-both-pill-background', - c.luminance(branchStatusPillLuminance).toString(), - ); - - if (e.isInitializing) return; - - this.state.theming = undefined; - this.setState(this.state, 'didChangeTheme'); - } - - @debug({ args: false, singleLine: true }) - protected override setState(state: State, type?: IpcNotification | InternalNotificationType): void { - const themingChanged = this.ensureTheming(state); - - this.state = state; - super.setState({ timestamp: state.timestamp, selectedRepository: state.selectedRepository }); - - this.updateStateCallback?.(this.state, type, themingChanged); - } - - private ensureTheming(state: State): boolean { - if (state.theming == null) { - state.theming = this.getGraphTheming(); - return true; - } - return false; - } - - private getGraphTheming(): { cssVariables: CssVariables; themeOpacityFactor: number } { - // this will be called on theme updated as well as on config updated since it is dependent on the column colors from config changes and the background color from the theme - const computedStyle = window.getComputedStyle(document.documentElement); - const bgColor = getCssVariable('--color-background', computedStyle); - - const mixedGraphColors: CssVariables = {}; - - let i = 0; - let color; - for (const [colorVar, colorDefault] of graphLaneThemeColors) { - color = getCssVariable(colorVar, computedStyle) || colorDefault; - - mixedGraphColors[`--column-${i}-color`] = color; - - mixedGraphColors[`--graph-color-${i}`] = color; - for (const mixInt of [15, 25, 45, 50]) { - mixedGraphColors[`--graph-color-${i}-bg${mixInt}`] = mix(bgColor, color, mixInt); - } - for (const mixInt of [10, 50]) { - mixedGraphColors[`--graph-color-${i}-f${mixInt}`] = opacity(color, mixInt); - } - - i++; - } - - const isHighContrastTheme = - document.body.classList.contains('vscode-high-contrast') || - document.body.classList.contains('vscode-high-contrast-light'); - - return { - cssVariables: { - '--app__bg0': bgColor, - '--panel__bg0': getCssVariable('--color-graph-background', computedStyle), - '--panel__bg1': getCssVariable('--color-graph-background2', computedStyle), - '--section-border': getCssVariable('--color-graph-background2', computedStyle), - - '--selected-row': getCssVariable('--color-graph-selected-row', computedStyle), - '--selected-row-border': isHighContrastTheme - ? `1px solid ${getCssVariable('--color-graph-contrast-border', computedStyle)}` - : 'none', - '--hover-row': getCssVariable('--color-graph-hover-row', computedStyle), - '--hover-row-border': isHighContrastTheme - ? `1px dashed ${getCssVariable('--color-graph-contrast-border', computedStyle)}` - : 'none', - - '--scrollable-scrollbar-thickness': getCssVariable('--graph-column-scrollbar-thickness', computedStyle), - '--scroll-thumb-bg': getCssVariable('--vscode-scrollbarSlider-background', computedStyle), - - '--scroll-marker-head-color': getCssVariable('--color-graph-scroll-marker-head', computedStyle), - '--scroll-marker-upstream-color': getCssVariable('--color-graph-scroll-marker-upstream', computedStyle), - '--scroll-marker-highlights-color': getCssVariable( - '--color-graph-scroll-marker-highlights', - computedStyle, - ), - '--scroll-marker-local-branches-color': getCssVariable( - '--color-graph-scroll-marker-local-branches', - computedStyle, - ), - '--scroll-marker-remote-branches-color': getCssVariable( - '--color-graph-scroll-marker-remote-branches', - computedStyle, - ), - '--scroll-marker-stashes-color': getCssVariable('--color-graph-scroll-marker-stashes', computedStyle), - '--scroll-marker-tags-color': getCssVariable('--color-graph-scroll-marker-tags', computedStyle), - '--scroll-marker-selection-color': getCssVariable( - '--color-graph-scroll-marker-selection', - computedStyle, - ), - '--scroll-marker-pull-requests-color': getCssVariable( - '--color-graph-scroll-marker-pull-requests', - computedStyle, - ), - - '--stats-added-color': getCssVariable('--color-graph-stats-added', computedStyle), - '--stats-deleted-color': getCssVariable('--color-graph-stats-deleted', computedStyle), - '--stats-files-color': getCssVariable('--color-graph-stats-files', computedStyle), - '--stats-bar-border-radius': getCssVariable('--graph-stats-bar-border-radius', computedStyle), - '--stats-bar-height': getCssVariable('--graph-stats-bar-height', computedStyle), - - '--text-selected': getCssVariable('--color-graph-text-selected', computedStyle), - '--text-selected-row': getCssVariable('--color-graph-text-selected-row', computedStyle), - '--text-hovered': getCssVariable('--color-graph-text-hovered', computedStyle), - '--text-dimmed-selected': getCssVariable('--color-graph-text-dimmed-selected', computedStyle), - '--text-dimmed': getCssVariable('--color-graph-text-dimmed', computedStyle), - '--text-normal': getCssVariable('--color-graph-text-normal', computedStyle), - '--text-secondary': getCssVariable('--color-graph-text-secondary', computedStyle), - '--text-disabled': getCssVariable('--color-graph-text-disabled', computedStyle), - - '--text-accent': getCssVariable('--color-link-foreground', computedStyle), - '--text-inverse': getCssVariable('--vscode-input-background', computedStyle), - '--text-bright': getCssVariable('--vscode-input-background', computedStyle), - ...mixedGraphColors, - }, - themeOpacityFactor: parseInt(getCssVariable('--graph-theme-opacity-factor', computedStyle)) || 1, - }; - } - - private onColumnsChanged(settings: GraphColumnsConfig) { - this.sendCommand(UpdateColumnsCommand, { - config: settings, - }); - } - - private onRefsVisibilityChanged(refs: GraphExcludedRef[], visible: boolean) { - this.sendCommand(UpdateRefsVisibilityCommand, { - refs: refs, - visible: visible, - }); - } - - private onChooseRepository() { - this.sendCommand(ChooseRepositoryCommand, undefined); - } - - private onDoubleClickRef(ref: GraphRef, metadata?: GraphRefMetadataItem) { - this.sendCommand(DoubleClickedCommandType, { - type: 'ref', - ref: ref, - metadata: metadata, - }); - } - - private onDoubleClickRow(row: GraphRow, preserveFocus?: boolean) { - this.sendCommand(DoubleClickedCommandType, { - type: 'row', - row: { id: row.sha, type: row.type as GitGraphRowType }, - preserveFocus: preserveFocus, - }); - } - - private async onHoverRowPromise(row: GraphRow) { - try { - const request = await this.sendRequest(GetRowHoverRequest, { - type: row.type as GitGraphRowType, - id: row.sha, - }); - this._telemetry.sendEvent({ name: 'graph/row/hovered', data: {} }); - return request; - } catch (ex) { - return { id: row.sha, markdown: { status: 'rejected' as const, reason: ex } }; - } - } - - private async onJumpToRefPromise(alt: boolean): Promise<{ name: string; sha: string } | undefined> { - try { - // Assuming we have a command to get the ref details - const rsp = await this.sendRequest(ChooseRefRequest, { alt: alt }); - this._telemetry.sendEvent({ name: 'graph/action/jumpTo', data: { alt: alt } }); - return rsp; - } catch { - return undefined; - } - } - - private onGetMissingAvatars(emails: GraphAvatars) { - this.sendCommand(GetMissingAvatarsCommand, { emails: emails }); - } - - private onGetMissingRefsMetadata(metadata: GraphMissingRefsMetadata) { - this.sendCommand(GetMissingRefsMetadataCommand, { metadata: metadata }); - } - - private onGetMoreRows(sha?: string) { - this.sendCommand(GetMoreRowsCommand, { id: sha }); - } - - onOpenPullRequest(pr: NonNullable['pr']>): void { - this.sendCommand(OpenPullRequestDetailsCommand, { id: pr.id }); - } - - private async onSearch(search: SearchQuery | undefined, options?: { limit?: number }) { - if (search == null) { - this.state.searchResults = undefined; - } - try { - const rsp = await this.sendRequest(SearchRequest, { search: search, limit: options?.limit }); - this.updateSearchResultState(rsp); - } catch { - this.state.searchResults = undefined; - } - } - - private async onSearchPromise(search: SearchQuery, options?: { limit?: number; more?: boolean }) { - try { - const rsp = await this.sendRequest(SearchRequest, { - search: search, - limit: options?.limit, - more: options?.more, - }); - this.updateSearchResultState(rsp); - return rsp; - } catch { - return undefined; - } - } - - private onSearchOpenInView(search: SearchQuery) { - this.sendCommand(SearchOpenInViewCommand, { search: search }); - } - - private async onEnsureRowPromise(id: string, select: boolean) { - try { - return await this.sendRequest(EnsureRowRequest, { id: id, select: select }); - } catch { - return undefined; - } - } - - private onExcludeTypesChanged(key: keyof GraphExcludeTypes, value: boolean) { - this.sendCommand(UpdateExcludeTypesCommand, { key: key, value: value }); - } - - private onRefIncludesChanged(branchesVisibility: GraphBranchesVisibility, refs?: GraphRefOptData[]) { - this.sendCommand(UpdateIncludedRefsCommand, { branchesVisibility: branchesVisibility, refs: refs }); - } - - private onGraphConfigurationChanged(changes: UpdateGraphConfigurationParams['changes']) { - this.sendCommand(UpdateGraphConfigurationCommand, { changes: changes }); - } - - private onGraphSearchModeChanged(searchMode: GraphSearchMode) { - this.sendCommand(UpdateGraphSearchModeCommand, { searchMode: searchMode }); - } - - private onSelectionChanged(rows: GraphRow[]) { - const selection = rows.filter(r => r != null).map(r => ({ id: r.sha, type: r.type as GitGraphRowType })); - this._telemetry.sendEvent({ name: 'graph/row/selected', data: { rows: selection.length } }); - this.sendCommand(UpdateSelectionCommand, { - selection: selection, - }); - } - - private registerUpdateStateCallback(updateState: UpdateStateCallback): () => void { - this.updateStateCallback = updateState; - - return () => { - this.updateStateCallback = undefined; - }; - } - - private updateSearchResultState(params: DidSearchParams) { - this.state.searchResults = params.results; - if (params.selectedRows != null) { - this.state.selectedRows = params.selectedRows; - } - this.setState(this.state, DidSearchNotification); - } -} - -new GraphApp(); diff --git a/src/webviews/apps/plus/graph/hover/graphHover.react.tsx b/src/webviews/apps/plus/graph/hover/graphHover.react.tsx deleted file mode 100644 index 59db4570152c0..0000000000000 --- a/src/webviews/apps/plus/graph/hover/graphHover.react.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { reactWrapper } from '../../../shared/components/helpers/react-wrapper'; -import { GlGraphHover as GlGraphHoverWC } from './graphHover'; - -export interface GlGraphHover extends GlGraphHoverWC {} -export const GlGraphHover = reactWrapper(GlGraphHoverWC, { tagName: 'gl-graph-hover' }); diff --git a/src/webviews/apps/plus/graph/hover/graphHover.ts b/src/webviews/apps/plus/graph/hover/graphHover.ts index f5f0ecad9b948..da01927b89e94 100644 --- a/src/webviews/apps/plus/graph/hover/graphHover.ts +++ b/src/webviews/apps/plus/graph/hover/graphHover.ts @@ -7,9 +7,8 @@ import { debounce } from '../../../../../system/function'; import { getSettledValue, isPromise } from '../../../../../system/promise'; import type { DidGetRowHoverParams } from '../../../../plus/graph/protocol'; import { GlElement } from '../../../shared/components/element'; -import type { GlPopover } from '../../../shared/components/overlays/popover.react'; import '../../../shared/components/markdown/markdown'; -import '../../../shared/components/overlays/popover'; +import type { GlPopover } from '../../../shared/components/overlays/popover'; declare global { interface HTMLElementTagNameMap { diff --git a/src/webviews/apps/plus/graph/minimap/minimap-container.react.tsx b/src/webviews/apps/plus/graph/minimap/minimap-container.react.tsx deleted file mode 100644 index 876a0774195e6..0000000000000 --- a/src/webviews/apps/plus/graph/minimap/minimap-container.react.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { EventName } from '@lit/react'; -import type { CustomEventType } from '../../../shared/components/element'; -import { reactWrapper } from '../../../shared/components/helpers/react-wrapper'; -import { GlGraphMinimapContainer as GlGraphMinimapContainerWC } from './minimap-container'; - -export interface GlGraphMinimapContainer extends GlGraphMinimapContainerWC {} -export const GlGraphMinimapContainer = reactWrapper(GlGraphMinimapContainerWC, { - tagName: 'gl-graph-minimap-container', - events: { - onSelected: 'gl-graph-minimap-selected' as EventName>, - }, -}); diff --git a/src/webviews/apps/plus/graph/minimap/minimap.react.tsx b/src/webviews/apps/plus/graph/minimap/minimap.react.tsx deleted file mode 100644 index 50b581276e969..0000000000000 --- a/src/webviews/apps/plus/graph/minimap/minimap.react.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { EventName } from '@lit/react'; -import type { CustomEventType } from '../../../shared/components/element'; -import { reactWrapper } from '../../../shared/components/helpers/react-wrapper'; -import { GlGraphMinimap as GlGraphMinimapWC } from './minimap'; - -export interface GlGraphMinimap extends GlGraphMinimapWC {} -export const GlGraphMinimap = reactWrapper(GlGraphMinimapWC, { - tagName: 'gl-graph-minimap', - events: { - onSelected: 'gl-graph-minimap-selected' as EventName>, - }, -}); diff --git a/src/webviews/apps/plus/graph/sidebar/sidebar.react.tsx b/src/webviews/apps/plus/graph/sidebar/sidebar.react.tsx deleted file mode 100644 index 78bc35ff4b7e4..0000000000000 --- a/src/webviews/apps/plus/graph/sidebar/sidebar.react.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { reactWrapper } from '../../../shared/components/helpers/react-wrapper'; -import { GlGraphSideBar as GlGraphSideBarWC } from './sidebar'; - -export interface GlGraphSideBar extends GlGraphSideBarWC {} -export const GlGraphSideBar = reactWrapper(GlGraphSideBarWC, { tagName: 'gl-graph-sidebar' }); diff --git a/src/webviews/apps/plus/graph/sidebar/sidebar.ts b/src/webviews/apps/plus/graph/sidebar/sidebar.ts index a365f21817c5d..7fb89c2336562 100644 --- a/src/webviews/apps/plus/graph/sidebar/sidebar.ts +++ b/src/webviews/apps/plus/graph/sidebar/sidebar.ts @@ -1,15 +1,17 @@ import { consume } from '@lit/context'; import { Task } from '@lit/task'; import { css, html, LitElement, nothing } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; +import type { State } from '../../../../plus/graph/protocol'; import { DidChangeNotification, GetCountsRequest } from '../../../../plus/graph/protocol'; +import '../../../shared/components/code-icon'; +import '../../../shared/components/overlays/tooltip'; import { ipcContext } from '../../../shared/context'; import type { Disposable } from '../../../shared/events'; import type { HostIpc } from '../../../shared/ipc'; -import '../../../shared/components/code-icon'; -import '../../../shared/components/overlays/tooltip'; import { emitTelemetrySentEvent } from '../../../shared/telemetry'; +import { stateContext } from '../context'; interface Icon { type: IconTypes; @@ -72,16 +74,25 @@ export class GlGraphSideBar extends LitElement { } `; - @property({ type: Boolean }) - enabled = true; + get enabled() { + return this._state.config?.sidebar; + } - @property({ type: Array }) - include?: IconTypes[]; + get include(): undefined | IconTypes[] { + const repo = this._state.repositories?.find(item => item.path === this._state.selectedRepository); + return repo?.isVirtual + ? (['branches', 'remotes', 'tags'] as const) + : (['branches', 'remotes', 'tags', 'stashes', 'worktrees'] as const); + } @consume({ context: ipcContext }) - private _ipc!: HostIpc; + private readonly _ipc!: HostIpc; + + @consume({ context: stateContext, subscribe: true }) + private readonly _state!: State; + private _disposable: Disposable | undefined; - private _countsTask = new Task(this, { + private readonly _countsTask = new Task(this, { args: () => [this.fetchCounts()], task: ([counts]) => counts, autoRun: false, diff --git a/src/webviews/apps/plus/graph/stateProvider.ts b/src/webviews/apps/plus/graph/stateProvider.ts new file mode 100644 index 0000000000000..01abf869bea81 --- /dev/null +++ b/src/webviews/apps/plus/graph/stateProvider.ts @@ -0,0 +1,341 @@ +import type { CssVariables } from '@gitkraken/gitkraken-components'; +import { ContextProvider, createContext } from '@lit/context'; +import type { ReactiveControllerHost } from 'lit'; +import type { SearchQuery } from '../../../../constants.search'; +import { getLogScope, setLogScopeExit } from '../../../../system/logger.scope'; +import type { + GraphSearchResults, + GraphSearchResultsError, + GraphSelectedRows, + State, +} from '../../../plus/graph/protocol'; +import { + DidChangeAvatarsNotification, + DidChangeBranchStateNotification, + DidChangeColumnsNotification, + DidChangeGraphConfigurationNotification, + DidChangeNotification, + DidChangeRefsMetadataNotification, + DidChangeRefsVisibilityNotification, + DidChangeRepoConnectionNotification, + DidChangeRowsNotification, + DidChangeRowsStatsNotification, + DidChangeScrollMarkersNotification, + DidChangeSelectionNotification, + DidChangeSubscriptionNotification, + DidChangeWorkingTreeNotification, + DidFetchNotification, + DidSearchNotification, + DidStartFeaturePreviewNotification, +} from '../../../plus/graph/protocol'; +import { DidChangeHostWindowFocusNotification } from '../../../protocol'; +import type { StateProvider } from '../../shared/app'; +import { signalObjectState, signalState } from '../../shared/components/signal-utils'; +import type { LoggerContext } from '../../shared/context'; +import type { Disposable } from '../../shared/events'; +import type { HostIpc } from '../../shared/ipc'; +import { stateContext } from './context'; + +type ReactiveElementHost = Partial & HTMLElement; + +interface AppState { + activeDay?: number; + activeRow?: string; + visibleDays?: { + top: number; + bottom: number; + }; + theming?: { cssVariables: CssVariables; themeOpacityFactor: number }; +} + +function getSearchResultModel(searchResults: State['searchResults']): { + results: undefined | GraphSearchResults; + resultsError: undefined | GraphSearchResultsError; +} { + let results: undefined | GraphSearchResults; + let resultsError: undefined | GraphSearchResultsError; + if (searchResults != null) { + if ('error' in searchResults) { + resultsError = searchResults; + } else { + results = searchResults; + } + } + return { results: results, resultsError: resultsError }; +} + +export class GraphAppState implements AppState { + @signalState() + accessor activeDay: number | undefined; + + @signalState() + accessor activeRow: string | undefined = undefined; + + @signalState(false) + accessor loading = false; + + @signalState(false) + accessor searching = false; + + @signalObjectState() + accessor visibleDays: AppState['visibleDays']; + + @signalObjectState() + accessor theming: AppState['theming']; + + @signalObjectState( + { query: '' }, + { + afterChange: target => { + target.searchResultsHidden = false; + }, + }, + ) + accessor filter!: SearchQuery; + + @signalState(false) + accessor searchResultsHidden = false; + + @signalState() + accessor searchResultsResponse: undefined | GraphSearchResults | GraphSearchResultsError; + + get searchResults() { + return getSearchResultModel(this.searchResultsResponse).results; + } + + get searchResultsError() { + return getSearchResultModel(this.searchResultsResponse).resultsError; + } + + @signalState() + accessor selectedRows: undefined | GraphSelectedRows; +} + +export const graphStateContext = createContext('graphState'); + +export class GraphStateProvider implements StateProvider { + private readonly disposable: Disposable; + private readonly provider: ContextProvider<{ __context__: State }, ReactiveElementHost>; + + private readonly _state: State; + get state() { + return this._state; + } + + private updateState(partial: Partial) { + for (const key in partial) { + // @ts-expect-error dynamic object key ejection doesn't work in typescript + this._state[key] = partial[key]; + } + this.options.onStateUpdate?.(partial); + this.provider.setValue(this._state, true); + } + + constructor( + host: ReactiveElementHost, + state: State, + private readonly _ipc: HostIpc, + private readonly _logger: LoggerContext, + private readonly options: { onStateUpdate?: (partial: Partial) => void } = {}, + ) { + this._state = state; + this.provider = new ContextProvider(host, { context: stateContext, initialValue: state }); + + this.disposable = this._ipc.onReceiveMessage(msg => { + const scope = getLogScope(); + + const updates: Partial = {}; + switch (true) { + case DidChangeNotification.is(msg): + this.updateState(msg.params.state); + break; + + case DidFetchNotification.is(msg): + this._state.lastFetched = msg.params.lastFetched; + this.updateState({ lastFetched: msg.params.lastFetched }); + break; + + case DidChangeAvatarsNotification.is(msg): + this.updateState({ avatars: msg.params.avatars }); + break; + case DidStartFeaturePreviewNotification.is(msg): + this._state.featurePreview = msg.params.featurePreview; + this._state.allowed = msg.params.allowed; + this.updateState({ + featurePreview: msg.params.featurePreview, + allowed: msg.params.allowed, + }); + break; + case DidChangeBranchStateNotification.is(msg): + this.updateState({ + branchState: msg.params.branchState, + }); + break; + + case DidChangeHostWindowFocusNotification.is(msg): + this.updateState({ + windowFocused: msg.params.focused, + }); + break; + + case DidChangeColumnsNotification.is(msg): + this.updateState({ + columns: msg.params.columns, + context: { + ...this._state.context, + header: msg.params.context, + settings: msg.params.settingsContext, + }, + }); + break; + + case DidChangeRefsVisibilityNotification.is(msg): + this.updateState({ + branchesVisibility: msg.params.branchesVisibility, + excludeRefs: msg.params.excludeRefs, + excludeTypes: msg.params.excludeTypes, + includeOnlyRefs: msg.params.includeOnlyRefs, + }); + break; + + case DidChangeRefsMetadataNotification.is(msg): + this.updateState({ + refsMetadata: msg.params.metadata, + }); + break; + + case DidChangeRowsNotification.is(msg): { + let rows; + if ( + msg.params.rows.length && + msg.params.paging?.startingCursor != null && + this._state.rows != null + ) { + const previousRows = this._state.rows; + const lastId = previousRows[previousRows.length - 1]?.sha; + + let previousRowsLength = previousRows.length; + const newRowsLength = msg.params.rows.length; + + this._logger.log( + scope, + `paging in ${newRowsLength} rows into existing ${previousRowsLength} rows at ${msg.params.paging.startingCursor} (last existing row: ${lastId})`, + ); + + rows = []; + // Preallocate the array to avoid reallocations + rows.length = previousRowsLength + newRowsLength; + + if (msg.params.paging.startingCursor !== lastId) { + this._logger.log( + scope, + `searching for ${msg.params.paging.startingCursor} in existing rows`, + ); + + let i = 0; + let row; + for (row of previousRows) { + rows[i++] = row; + if (row.sha === msg.params.paging.startingCursor) { + this._logger.log( + scope, + `found ${msg.params.paging.startingCursor} in existing rows`, + ); + + previousRowsLength = i; + + if (previousRowsLength !== previousRows.length) { + // If we stopped before the end of the array, we need to trim it + rows.length = previousRowsLength + newRowsLength; + } + + break; + } + } + } else { + for (let i = 0; i < previousRowsLength; i++) { + rows[i] = previousRows[i]; + } + } + + for (let i = 0; i < newRowsLength; i++) { + rows[previousRowsLength + i] = msg.params.rows[i]; + } + } else { + this._logger.log(scope, `setting to ${msg.params.rows.length} rows`); + + if (msg.params.rows.length === 0) { + rows = this._state.rows; + } else { + rows = msg.params.rows; + } + } + + updates.avatars = msg.params.avatars; + updates.downstreams = msg.params.downstreams; + if (msg.params.refsMetadata !== undefined) { + updates.refsMetadata = msg.params.refsMetadata; + } + updates.rows = rows; + updates.paging = msg.params.paging; + if (msg.params.rowsStats != null) { + updates.rowsStats = { ...this._state.rowsStats, ...msg.params.rowsStats }; + } + updates.rowsStatsLoading = msg.params.rowsStatsLoading; + if (msg.params.selectedRows != null) { + updates.selectedRows = msg.params.selectedRows; + } + updates.loading = false; + this.updateState(updates); + setLogScopeExit(scope, ` \u2022 rows=${this._state.rows?.length ?? 0}`); + break; + } + case DidChangeRowsStatsNotification.is(msg): + this.updateState({ + rowsStats: { ...this._state.rowsStats, ...msg.params.rowsStats }, + rowsStatsLoading: msg.params.rowsStatsLoading, + }); + break; + + case DidChangeScrollMarkersNotification.is(msg): + this.updateState({ context: { ...this._state.context, settings: msg.params.context } }); + break; + + case DidSearchNotification.is(msg): + if (msg.params.selectedRows != null) { + updates.selectedRows = msg.params.selectedRows; + } + updates.searchResults = msg.params.results; + this.updateState(updates); + break; + + case DidChangeSelectionNotification.is(msg): + this.updateState({ selectedRows: msg.params.selection }); + break; + + case DidChangeGraphConfigurationNotification.is(msg): + this.updateState({ config: msg.params.config }); + break; + + case DidChangeSubscriptionNotification.is(msg): + this.updateState({ + subscription: msg.params.subscription, + allowed: msg.params.allowed, + }); + break; + + case DidChangeWorkingTreeNotification.is(msg): + this.updateState({ workingTreeStats: msg.params.stats }); + break; + + case DidChangeRepoConnectionNotification.is(msg): + this.updateState({ repositories: msg.params.repositories }); + break; + } + }); + } + + dispose(): void { + this.disposable.dispose(); + } +} diff --git a/src/webviews/apps/plus/shared/components/account-chip.ts b/src/webviews/apps/plus/shared/components/account-chip.ts index 8d987230437ed..1ffaa8e18204e 100644 --- a/src/webviews/apps/plus/shared/components/account-chip.ts +++ b/src/webviews/apps/plus/shared/components/account-chip.ts @@ -17,13 +17,12 @@ import { createCommandLink } from '../../../../../system/commands'; import { pluralize } from '../../../../../system/string'; import type { State } from '../../../../home/protocol'; import { stateContext } from '../../../home/context'; -import type { GlPopover } from '../../../shared/components/overlays/popover.react'; -import { elementBase, linkBase } from '../../../shared/components/styles/lit/base.css'; -import { chipStyles } from './chipStyles'; import '../../../shared/components/button'; import '../../../shared/components/button-container'; import '../../../shared/components/code-icon'; -import '../../../shared/components/overlays/popover'; +import type { GlPopover } from '../../../shared/components/overlays/popover'; +import { elementBase, linkBase } from '../../../shared/components/styles/lit/base.css'; +import { chipStyles } from './chipStyles'; @customElement('gl-account-chip') export class GLAccountChip extends LitElement { @@ -224,14 +223,14 @@ export class GLAccountChip extends LitElement { ]; @query('#chip') - private _chip!: HTMLElement; + private readonly _chip!: HTMLElement; @query('gl-popover') - private _popover!: GlPopover; + private readonly _popover!: GlPopover; @consume({ context: stateContext, subscribe: true }) @state() - private _state!: State; + private readonly _state!: State; private get accountAvatar() { return this.hasAccount && this._state.avatar; diff --git a/src/webviews/apps/plus/shared/components/merge-rebase-status.react.ts b/src/webviews/apps/plus/shared/components/merge-rebase-status.react.ts deleted file mode 100644 index c7298bcda148d..0000000000000 --- a/src/webviews/apps/plus/shared/components/merge-rebase-status.react.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { reactWrapper } from '../../../shared/components/helpers/react-wrapper'; -import { GlMergeConflictWarning as GlMergeConflictWarningWC } from './merge-rebase-status'; - -export interface GlMergeConflictWarning extends GlMergeConflictWarningWC {} -export const GlMergeConflictWarning = reactWrapper(GlMergeConflictWarningWC, { tagName: 'gl-merge-rebase-status' }); diff --git a/src/webviews/apps/shared/app.ts b/src/webviews/apps/shared/app.ts index e0f009470ebf8..2877c84b9d40a 100644 --- a/src/webviews/apps/shared/app.ts +++ b/src/webviews/apps/shared/app.ts @@ -10,6 +10,8 @@ import { GlElement } from './components/element'; import { ipcContext, LoggerContext, loggerContext, telemetryContext, TelemetryContext } from './context'; import type { Disposable } from './events'; import { HostIpc } from './ipc'; +import type { ThemeChangeEvent } from './theme'; +import { computeThemeColors, onDidChangeTheme, watchThemeColors } from './theme'; export interface StateProvider extends Disposable { readonly state: State; @@ -41,6 +43,7 @@ export abstract class GlApp< @property({ type: Object, noAccessor: true }) private bootstrap!: State; + protected onThemeUpdated?(e: ThemeChangeEvent): void; get state(): State { return this._stateProvider.state; @@ -68,6 +71,13 @@ export abstract class GlApp< this._ipc.replaceIpcPromisesWithPromises(state); this.onPersistState(state); + const themeEvent = computeThemeColors(); + if (this.onThemeUpdated != null) { + this.onThemeUpdated(themeEvent); + this.disposables.push(watchThemeColors()); + this.disposables.push(onDidChangeTheme(this.onThemeUpdated, this)); + } + this.disposables.push( (this._stateProvider = this.createStateProvider(state, this._ipc)), this._ipc.onReceiveMessage(msg => { diff --git a/src/webviews/apps/shared/components/button.react.tsx b/src/webviews/apps/shared/components/button.react.tsx deleted file mode 100644 index e94fd061c9550..0000000000000 --- a/src/webviews/apps/shared/components/button.react.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { GlButton as GlButtonWC } from './button'; -import { reactWrapper } from './helpers/react-wrapper'; - -export interface GlButton extends GlButtonWC {} -export const GlButton = reactWrapper(GlButtonWC, { tagName: 'gl-button' }); diff --git a/src/webviews/apps/shared/components/checkbox/checkbox.react.ts b/src/webviews/apps/shared/components/checkbox/checkbox.react.ts deleted file mode 100644 index 5f594e450a012..0000000000000 --- a/src/webviews/apps/shared/components/checkbox/checkbox.react.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { reactWrapper } from '../helpers/react-wrapper'; -import { Checkbox, tagName } from './checkbox'; - -export const GlCheckbox = reactWrapper(Checkbox, { - tagName: tagName, - events: { - onChange: 'gl-change-value', - }, -}); diff --git a/src/webviews/apps/shared/components/checkbox/index.ts b/src/webviews/apps/shared/components/checkbox/index.ts index 037657f97d59f..8d78b3e23f250 100644 --- a/src/webviews/apps/shared/components/checkbox/index.ts +++ b/src/webviews/apps/shared/components/checkbox/index.ts @@ -1,2 +1 @@ -export { GlCheckbox } from './checkbox.react'; export * from './checkbox'; diff --git a/src/webviews/apps/shared/components/indicators/indicator.react.ts b/src/webviews/apps/shared/components/indicators/indicator.react.ts deleted file mode 100644 index 893c188bdd731..0000000000000 --- a/src/webviews/apps/shared/components/indicators/indicator.react.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { reactWrapper } from '../helpers/react-wrapper'; -import { GlIndicator as GlIndicatorWC, tagName } from './indicator'; - -export const GlIndicator = reactWrapper(GlIndicatorWC, { tagName: tagName }); diff --git a/src/webviews/apps/shared/components/integrations/connect.react.tsx b/src/webviews/apps/shared/components/integrations/connect.react.tsx deleted file mode 100644 index b8d6af394e41b..0000000000000 --- a/src/webviews/apps/shared/components/integrations/connect.react.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { reactWrapper } from '../helpers/react-wrapper'; -import { GlConnect as GlConnectWC } from './connect'; - -export interface GlConnect extends GlConnectWC {} -export const GlConnect = reactWrapper(GlConnectWC, { tagName: 'gl-connect' }); diff --git a/src/webviews/apps/shared/components/overlays/popover.react.tsx b/src/webviews/apps/shared/components/overlays/popover.react.tsx deleted file mode 100644 index f19612e9fb405..0000000000000 --- a/src/webviews/apps/shared/components/overlays/popover.react.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { reactWrapper } from '../helpers/react-wrapper'; -import { GlPopover as GlPopoverWC } from './popover'; - -export interface GlPopover extends GlPopoverWC {} -export const GlPopover = reactWrapper(GlPopoverWC, { tagName: 'gl-popover' }); diff --git a/src/webviews/apps/shared/components/radio/index.ts b/src/webviews/apps/shared/components/radio/index.ts index 07d013c261d77..1140e08e216fc 100644 --- a/src/webviews/apps/shared/components/radio/index.ts +++ b/src/webviews/apps/shared/components/radio/index.ts @@ -1,2 +1 @@ -export * from './radio.react'; export * from './radio'; diff --git a/src/webviews/apps/shared/components/radio/radio.react.ts b/src/webviews/apps/shared/components/radio/radio.react.ts deleted file mode 100644 index b226f6a035154..0000000000000 --- a/src/webviews/apps/shared/components/radio/radio.react.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { reactWrapper } from '../helpers/react-wrapper'; -import { Radio, tagName as radioTagName } from './radio'; -import { RadioGroup, tagName as radioGroupTagName } from './radio-group'; - -export const GlRadio = reactWrapper(Radio, { - tagName: radioTagName, -}); - -export const GlRadioGroup = reactWrapper(RadioGroup, { - tagName: radioGroupTagName, - events: { - onChange: 'gl-change-value', - }, -}); diff --git a/src/webviews/apps/shared/components/react/feature-badge.tsx b/src/webviews/apps/shared/components/react/feature-badge.tsx deleted file mode 100644 index 4d4e00ae302c0..0000000000000 --- a/src/webviews/apps/shared/components/react/feature-badge.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { GlFeatureBadge as GlFeatureBadgeWC } from '../feature-badge'; -import { reactWrapper } from '../helpers/react-wrapper'; - -export interface GlFeatureBadge extends GlFeatureBadgeWC {} -export const GlFeatureBadge = reactWrapper(GlFeatureBadgeWC, { tagName: 'gl-feature-badge' }); diff --git a/src/webviews/apps/shared/components/react/feature-gate.tsx b/src/webviews/apps/shared/components/react/feature-gate.tsx deleted file mode 100644 index 567e21f1e9fbe..0000000000000 --- a/src/webviews/apps/shared/components/react/feature-gate.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { GlFeatureGate as GlFeatureGateWC } from '../feature-gate'; -import { reactWrapper } from '../helpers/react-wrapper'; - -export interface GlFeatureGate extends GlFeatureGateWC {} -export const GlFeatureGate = reactWrapper(GlFeatureGateWC, { tagName: 'gl-feature-gate' }); diff --git a/src/webviews/apps/shared/components/react/issue-pull-request.tsx b/src/webviews/apps/shared/components/react/issue-pull-request.tsx deleted file mode 100644 index fe8fdc4486069..0000000000000 --- a/src/webviews/apps/shared/components/react/issue-pull-request.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { EventName } from '@lit/react'; -import type { CustomEventType } from '../element'; -import { reactWrapper } from '../helpers/react-wrapper'; -import { IssuePullRequest as IssuePullRequestWC } from '../rich/issue-pull-request'; - -export interface GlIssuePullRequest extends IssuePullRequestWC {} -export const GlIssuePullRequest = reactWrapper(IssuePullRequestWC, { - tagName: 'issue-pull-request', - events: { - onOpenDetails: 'gl-issue-pull-request-details' as EventName>, - }, -}); diff --git a/src/webviews/apps/shared/components/search/react.tsx b/src/webviews/apps/shared/components/search/react.tsx deleted file mode 100644 index a82dc15fc696b..0000000000000 --- a/src/webviews/apps/shared/components/search/react.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { EventName } from '@lit/react'; -import type { CustomEventType } from '../element'; -import { reactWrapper } from '../helpers/react-wrapper'; -import { GlSearchBox as GlSearchBoxWC } from './search-box'; - -export interface GlSearchBox extends GlSearchBoxWC {} -export const GlSearchBox = reactWrapper(GlSearchBoxWC, { - tagName: 'gl-search-box', - events: { - onChange: 'gl-search-inputchange' as EventName>, - onNavigate: 'gl-search-navigate' as EventName>, - onOpenInView: 'gl-search-openinview' as EventName>, - onSearchModeChange: 'gl-search-modechange' as EventName>, - }, -}); diff --git a/src/webviews/apps/shared/components/search/search-input.ts b/src/webviews/apps/shared/components/search/search-input.ts index 63f06d05a1d9d..f5c83d14c02e2 100644 --- a/src/webviews/apps/shared/components/search/search-input.ts +++ b/src/webviews/apps/shared/components/search/search-input.ts @@ -394,7 +394,6 @@ export class GlSearchInput extends GlElement {
{ return renderAsyncComputed(this.computed, config); } } + +export function signalState(initialValue?: T) { + return (_target: any, _fieldName: string, targetFields: { get?: () => T; set?: (v: T) => void }) => { + if (targetFields.get && targetFields.set) { + const signal = new Signal.State(initialValue); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + get: function () { + return signal.get(); + }, + set: function (value: any) { + signal.set(value); + }, + } as any; + } + throw new Error(`@signal can only be used on accessors or getters`); + }; +} + +export const signalObjectState = | undefined>( + initialValue?: T, + options: { afterChange?: (target: any, value: T) => void } = {}, +) => { + return (target: any, _fieldName: string, targetFields: { get?: () => T; set?: (v: T) => void }) => { + if (targetFields.get && targetFields.set) { + const signal = signalObject(initialValue); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + get: function () { + // I don't return {...signal} for optimization purpose + return signal; + }, + set: function (value: any) { + Object.entries(value).forEach(([key, value]) => { + signal[key] = value; + }); + options.afterChange?.(target, value); + }, + } as any; + } + throw new Error(`@signal can only be used on accessors or getters`); + }; +}; diff --git a/src/webviews/plus/graph/graphWebview.ts b/src/webviews/plus/graph/graphWebview.ts index bef9806ec6a57..9f4a6ba6f4eac 100644 --- a/src/webviews/plus/graph/graphWebview.ts +++ b/src/webviews/plus/graph/graphWebview.ts @@ -19,7 +19,6 @@ import type { } from '../../../config'; import { GlyphChars } from '../../../constants'; import { GlCommand } from '../../../constants.commands'; -import { HostingIntegrationId, IssueIntegrationId } from '../../../constants.integrations'; import type { StoredGraphFilters, StoredGraphRefType } from '../../../constants.storage'; import type { GraphShownTelemetryContext, GraphTelemetryContext, TelemetryEvents } from '../../../constants.telemetry'; import type { Container } from '../../../container'; @@ -91,11 +90,10 @@ import { getRepositoryIdentityForPullRequest, serializePullRequest, } from '../../../git/utils/pullRequest.utils'; -import { createReference, isGitReference } from '../../../git/utils/reference.utils'; +import { createReference } from '../../../git/utils/reference.utils'; import { isSha, shortenRevision } from '../../../git/utils/revision.utils'; import type { FeaturePreviewChangeEvent, SubscriptionChangeEvent } from '../../../plus/gk/subscriptionService'; import type { ConnectionStateChangeEvent } from '../../../plus/integrations/integrationService'; -import { remoteProviderIdToIntegrationId } from '../../../plus/integrations/integrationService'; import { getPullRequestBranchDeepLink } from '../../../plus/launchpad/launchpadProvider'; import type { AssociateIssueWithBranchCommandArgs } from '../../../plus/startWork/startWork'; import { ReferencesQuickPickIncludes, showReferencePicker } from '../../../quickpicks/referencePicker'; @@ -122,13 +120,21 @@ import { pauseOnCancelOrTimeoutMapTuplePromise, } from '../../../system/promise'; import { Stopwatch } from '../../../system/stopwatch'; -import { isWebviewItemContext, isWebviewItemGroupContext, serializeWebviewItemContext } from '../../../system/webview'; +import { serializeWebviewItemContext } from '../../../system/webview'; import { DeepLinkActionType } from '../../../uris/deepLinks/deepLink'; import { RepositoryFolderNode } from '../../../views/nodes/abstract/repositoryFolderNode'; import type { IpcCallMessageType, IpcMessage, IpcNotification } from '../../protocol'; import type { WebviewHost, WebviewProvider, WebviewShowingArgs } from '../../webviewProvider'; import type { WebviewPanelShowCommandArgs, WebviewShowOptions } from '../../webviewsController'; import { isSerializedState } from '../../webviewsController'; +import { + formatRepositories, + hasGitReference, + isGraphItemRefContext, + isGraphItemRefGroupContext, + isGraphItemTypedContext, + toGraphIssueTrackerType, +} from './graphWebviewUtils'; import type { BranchState, DidChangeRefsVisibilityParams, @@ -139,31 +145,20 @@ import type { GetMissingAvatarsParams, GetMissingRefsMetadataParams, GetMoreRowsParams, - GraphBranchContextValue, GraphColumnConfig, GraphColumnName, GraphColumnsConfig, GraphColumnsSettings, - GraphCommitContextValue, GraphComponentConfig, - GraphContributorContextValue, GraphExcludedRef, GraphExcludeRefs, GraphExcludeTypes, GraphHostingServiceType, GraphIncludeOnlyRef, GraphIncludeOnlyRefs, - GraphIssueContextValue, - GraphIssueTrackerType, GraphItemContext, - GraphItemGroupContext, - GraphItemRefContext, - GraphItemRefGroupContext, - GraphItemTypedContext, - GraphItemTypedContextValue, GraphMinimapMarkerTypes, GraphMissingRefsMetadataType, - GraphPullRequestContextValue, GraphPullRequestMetadata, GraphRefMetadata, GraphRefMetadataType, @@ -171,10 +166,7 @@ import type { GraphScrollMarkerTypes, GraphSearchResults, GraphSelectedRows, - GraphStashContextValue, - GraphTagContextValue, GraphUpstreamMetadata, - GraphUpstreamStatusContextValue, GraphWorkingTreeStats, OpenPullRequestDetailsParams, SearchOpenInViewParams, @@ -1292,7 +1284,7 @@ export class GraphWebviewProvider implements WebviewProvider = { active: T | undefined; selection: T[]; }; - -async function formatRepositories(repositories: Repository[]): Promise { - if (repositories.length === 0) return Promise.resolve([]); - - const result = await Promise.allSettled( - repositories.map>(async repo => { - const remotes = await repo.git.remotes().getBestRemotesWithProviders(); - const remote = remotes.find(r => r.hasIntegration()) ?? remotes[0]; - - return { - formattedName: repo.formattedName, - id: repo.id, - name: repo.name, - path: repo.path, - provider: remote?.provider - ? { - name: remote.provider.name, - integration: remote.hasIntegration() - ? { - id: remoteProviderIdToIntegrationId(remote.provider.id)!, - connected: remote.maybeIntegrationConnected ?? false, - } - : undefined, - icon: remote.provider.icon === 'remote' ? 'cloud' : remote.provider.icon, - url: remote.provider.url({ type: RemoteResourceType.Repo }), - } - : undefined, - isVirtual: repo.provider.virtual, - }; - }), - ); - return result.map(r => getSettledValue(r)).filter(r => r != null); -} - -function isGraphItemContext(item: unknown): item is GraphItemContext { - if (item == null) return false; - - return isWebviewItemContext(item) && (item.webview === 'gitlens.graph' || item.webview === 'gitlens.views.graph'); -} - -function isGraphItemGroupContext(item: unknown): item is GraphItemGroupContext { - if (item == null) return false; - - return ( - isWebviewItemGroupContext(item) && (item.webview === 'gitlens.graph' || item.webview === 'gitlens.views.graph') - ); -} - -function isGraphItemTypedContext( - item: unknown, - type: 'contributor', -): item is GraphItemTypedContext; -function isGraphItemTypedContext( - item: unknown, - type: 'pullrequest', -): item is GraphItemTypedContext; -function isGraphItemTypedContext( - item: unknown, - type: 'upstreamStatus', -): item is GraphItemTypedContext; -function isGraphItemTypedContext(item: unknown, type: 'issue'): item is GraphItemTypedContext; -function isGraphItemTypedContext( - item: unknown, - type: GraphItemTypedContextValue['type'], -): item is GraphItemTypedContext { - if (item == null) return false; - - return isGraphItemContext(item) && typeof item.webviewItemValue === 'object' && item.webviewItemValue.type === type; -} - -function isGraphItemRefGroupContext(item: unknown): item is GraphItemRefGroupContext { - if (item == null) return false; - - return ( - isGraphItemGroupContext(item) && - typeof item.webviewItemGroupValue === 'object' && - item.webviewItemGroupValue.type === 'refGroup' - ); -} - -function isGraphItemRefContext(item: unknown): item is GraphItemRefContext; -function isGraphItemRefContext(item: unknown, refType: 'branch'): item is GraphItemRefContext; -function isGraphItemRefContext( - item: unknown, - refType: 'revision', -): item is GraphItemRefContext; -function isGraphItemRefContext(item: unknown, refType: 'stash'): item is GraphItemRefContext; -function isGraphItemRefContext(item: unknown, refType: 'tag'): item is GraphItemRefContext; -function isGraphItemRefContext(item: unknown, refType?: GitReference['refType']): item is GraphItemRefContext { - if (item == null) return false; - - return ( - isGraphItemContext(item) && - typeof item.webviewItemValue === 'object' && - 'ref' in item.webviewItemValue && - (refType == null || item.webviewItemValue.ref.refType === refType) - ); -} - -export function hasGitReference(o: unknown): o is { ref: GitReference } { - if (o == null || typeof o !== 'object') return false; - if (!('ref' in o)) return false; - - return isGitReference(o.ref); -} - -function toGraphIssueTrackerType(id: string): GraphIssueTrackerType | undefined { - switch (id) { - case HostingIntegrationId.GitHub: - return 'github'; - case HostingIntegrationId.GitLab: - return 'gitlab'; - case IssueIntegrationId.Jira: - return 'jiraCloud'; - default: - return undefined; - } -} diff --git a/src/webviews/plus/graph/graphWebviewUtils.ts b/src/webviews/plus/graph/graphWebviewUtils.ts new file mode 100644 index 0000000000000..bab667f617e8f --- /dev/null +++ b/src/webviews/plus/graph/graphWebviewUtils.ts @@ -0,0 +1,147 @@ +import { HostingIntegrationId, IssueIntegrationId } from '../../../constants.integrations'; +import type { GitReference } from '../../../git/models/reference'; +import { RemoteResourceType } from '../../../git/models/remoteResource'; +import type { Repository } from '../../../git/models/repository'; +import { isGitReference } from '../../../git/utils/reference.utils'; +import { remoteProviderIdToIntegrationId } from '../../../plus/integrations/integrationService'; +import { getSettledValue } from '../../../system/promise'; +import { isWebviewItemContext, isWebviewItemGroupContext } from '../../../system/webview'; +import type { + GraphBranchContextValue, + GraphCommitContextValue, + GraphContributorContextValue, + GraphIssueContextValue, + GraphIssueTrackerType, + GraphItemContext, + GraphItemGroupContext, + GraphItemRefContext, + GraphItemRefGroupContext, + GraphItemTypedContext, + GraphItemTypedContextValue, + GraphPullRequestContextValue, + GraphRepository, + GraphStashContextValue, + GraphTagContextValue, + GraphUpstreamStatusContextValue, +} from './protocol'; + +export async function formatRepositories(repositories: Repository[]): Promise { + if (repositories.length === 0) return Promise.resolve([]); + + const result = await Promise.allSettled( + repositories.map>(async repo => { + const remotes = await repo.git.remotes().getBestRemotesWithProviders(); + const remote = remotes.find(r => r.hasIntegration()) ?? remotes[0]; + + return { + formattedName: repo.formattedName, + id: repo.id, + name: repo.name, + path: repo.path, + provider: remote?.provider + ? { + name: remote.provider.name, + integration: remote.hasIntegration() + ? { + id: remoteProviderIdToIntegrationId(remote.provider.id)!, + connected: remote.maybeIntegrationConnected ?? false, + } + : undefined, + icon: remote.provider.icon === 'remote' ? 'cloud' : remote.provider.icon, + url: remote.provider.url({ type: RemoteResourceType.Repo }), + } + : undefined, + isVirtual: repo.provider.virtual, + }; + }), + ); + return result.map(r => getSettledValue(r)).filter(r => r != null); +} +function isGraphItemContext(item: unknown): item is GraphItemContext { + if (item == null) return false; + + return isWebviewItemContext(item) && (item.webview === 'gitlens.graph' || item.webview === 'gitlens.views.graph'); +} +function isGraphItemGroupContext(item: unknown): item is GraphItemGroupContext { + if (item == null) return false; + + return ( + isWebviewItemGroupContext(item) && (item.webview === 'gitlens.graph' || item.webview === 'gitlens.views.graph') + ); +} +export function isGraphItemTypedContext( + item: unknown, + type: 'contributor', +): item is GraphItemTypedContext; +export function isGraphItemTypedContext( + item: unknown, + type: 'pullrequest', +): item is GraphItemTypedContext; +export function isGraphItemTypedContext( + item: unknown, + type: 'upstreamStatus', +): item is GraphItemTypedContext; +export function isGraphItemTypedContext( + item: unknown, + type: 'issue', +): item is GraphItemTypedContext; +export function isGraphItemTypedContext( + item: unknown, + type: GraphItemTypedContextValue['type'], +): item is GraphItemTypedContext { + if (item == null) return false; + + return isGraphItemContext(item) && typeof item.webviewItemValue === 'object' && item.webviewItemValue.type === type; +} +export function isGraphItemRefGroupContext(item: unknown): item is GraphItemRefGroupContext { + if (item == null) return false; + + return ( + isGraphItemGroupContext(item) && + typeof item.webviewItemGroupValue === 'object' && + item.webviewItemGroupValue.type === 'refGroup' + ); +} +export function isGraphItemRefContext(item: unknown): item is GraphItemRefContext; +export function isGraphItemRefContext( + item: unknown, + refType: 'branch', +): item is GraphItemRefContext; +export function isGraphItemRefContext( + item: unknown, + refType: 'revision', +): item is GraphItemRefContext; +export function isGraphItemRefContext( + item: unknown, + refType: 'stash', +): item is GraphItemRefContext; +export function isGraphItemRefContext(item: unknown, refType: 'tag'): item is GraphItemRefContext; +export function isGraphItemRefContext(item: unknown, refType?: GitReference['refType']): item is GraphItemRefContext { + if (item == null) return false; + + return ( + isGraphItemContext(item) && + typeof item.webviewItemValue === 'object' && + 'ref' in item.webviewItemValue && + (refType == null || item.webviewItemValue.ref.refType === refType) + ); +} + +export function hasGitReference(o: unknown): o is { ref: GitReference } { + if (o == null || typeof o !== 'object') return false; + if (!('ref' in o)) return false; + + return isGitReference(o.ref); +} +export function toGraphIssueTrackerType(id: string): GraphIssueTrackerType | undefined { + switch (id) { + case HostingIntegrationId.GitHub: + return 'github'; + case HostingIntegrationId.GitLab: + return 'gitlab'; + case IssueIntegrationId.Jira: + return 'jiraCloud'; + default: + return undefined; + } +} diff --git a/src/webviews/plus/graph/protocol.ts b/src/webviews/plus/graph/protocol.ts index 9b35e9e684e26..6635922477411 100644 --- a/src/webviews/plus/graph/protocol.ts +++ b/src/webviews/plus/graph/protocol.ts @@ -1,5 +1,4 @@ import type { - CssVariables, ExcludeByType, ExcludeRefsById, GraphColumnSetting, @@ -125,15 +124,6 @@ export interface State extends WebviewState { excludeTypes?: GraphExcludeTypes; includeOnlyRefs?: GraphIncludeOnlyRefs; featurePreview?: FeaturePreview; - - // Props below are computed in the webview (not passed) - activeDay?: number; - activeRow?: string; - visibleDays?: { - top: number; - bottom: number; - }; - theming?: { cssVariables: CssVariables; themeOpacityFactor: number }; } export interface BranchState extends GitTrackingState { diff --git a/webpack.config.mjs b/webpack.config.mjs index 358d3a9e6bfaf..9cac17b5e84ae 100644 --- a/webpack.config.mjs +++ b/webpack.config.mjs @@ -411,7 +411,7 @@ function getWebviewsConfig(mode, env) { context: basePath, entry: { commitDetails: './commitDetails/commitDetails.ts', - graph: './plus/graph/graph.tsx', + graph: './plus/graph/graph.ts', home: './home/home.ts', rebase: './rebase/rebase.ts', settings: './settings/settings.ts',