diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 529a23ca9edf4..18bd530d5a2b4 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -17,22 +17,25 @@ 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.30.0 (https://github.com/open-telemetry/opentelemetry-js) -15. @shoelace-style/shoelace version 2.20.0 (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.7 (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 16.8.4 (https://github.com/facebook/react) -27. react version 16.8.4 (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.20.0 (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.7 (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 16.14.0 (https://github.com/facebook/react) +28. react-dom version 19.0.0 (https://github.com/facebook/react) +29. react version 16.14.0 (https://github.com/facebook/react) +30. react version 19.0.0 (https://github.com/facebook/react) +31. signal-utils version 0.21.1 (https://github.com/proposal-signals/signal-utils) +32. slug version 10.0.0 (https://github.com/Trott/slug) +33. sortablejs version 1.15.0 (https://github.com/SortableJS/Sortable) %% @gk-nzaytsev/fast-string-truncated-width NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -1324,6 +1327,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 @@ -2152,6 +2285,33 @@ SOFTWARE. ========================================= END OF react-dom NOTICES AND INFORMATION +%% react-dom NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +========================================= +END OF react-dom NOTICES AND INFORMATION + %% react NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -2179,6 +2339,33 @@ SOFTWARE. ========================================= END OF react NOTICES AND INFORMATION +%% react NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +========================================= +END OF react NOTICES AND INFORMATION + %% signal-utils NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index e8e81425db047..de4372d239afa 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -919,6 +919,7 @@ or 'context.config.dateStyle': 'absolute' | 'relative', 'context.config.defaultItemLimit': number, 'context.config.dimMergeCommits': boolean, + 'context.config.experimental.renderer.enabled': boolean, 'context.config.highlightRowsOnRefHover': boolean, 'context.config.issues.enabled': boolean, 'context.config.layout': 'editor' | 'panel', diff --git a/package.json b/package.json index 204cdd3dcdb14..36a92c3a3d8d2 100644 --- a/package.json +++ b/package.json @@ -1057,6 +1057,16 @@ "scope": "window", "order": 300 }, + "gitlens.graph.experimental.renderer.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "(Experimental) Specifies whether to use the new renderer for the _Commit Graph_. Requires a restart", + "scope": "window", + "order": 301, + "tags": [ + "experimental" + ] + }, "gitlens.graph.branchesVisibility": { "type": "string", "enum": [ @@ -21402,13 +21412,13 @@ "analyze:bundle": "webpack --mode production --env analyzeBundle", "analyze:bundle:extension": "webpack --mode production --config-name extension:node --env analyzeBundle", "analyze:bundle:extension:browser": "webpack --mode production --config-name extension:webworker --env analyzeBundle", - "analyze:bundle:webviews": "webpack --mode production --config-name webviews --env analyzeBundle", + "analyze:bundle:webviews": "webpack --mode production --config-name webviews:common --config-name webviews --config-name webviews:graph-next --env analyzeBundle", "analyze:deps": "webpack --env analyzeDeps", "build": "webpack --mode development", "build:quick": "webpack --mode development --env skipLint", "build:extension": "webpack --mode development --config-name extension:node", "build:extension:browser": "webpack --mode development --config-name extension:webworker", - "build:webviews": "webpack --mode development --config-name webviews", + "build:webviews": "webpack --mode development --config-name webviews:common --config-name webviews --config-name webviews:graph-next", "build:icons": "pnpm run icons:svgo && pnpm fantasticon && pnpm run icons:apply && pnpm run icons:export", "build:tests": "node ./scripts/esbuild.tests.mjs", "// Extracts the contributions from package.json into contributions.json": "//", @@ -21465,6 +21475,7 @@ }, "dependencies": { "@gitkraken/gitkraken-components": "10.7.0", + "@gitkraken/gitkraken-components-next": "npm:@gitkraken/gitkraken-components@11.0.0-vnext.3", "@gitkraken/provider-apis": "0.26.2", "@gitkraken/shared-web-components": "0.1.1-rc.15", "@gk-nzaytsev/fast-string-truncated-width": "1.1.0", @@ -21481,6 +21492,7 @@ "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "@opentelemetry/semantic-conventions": "1.30.0", + "@r2wc/react-to-web-component": "^2.0.4", "@shoelace-style/shoelace": "2.20.0", "@vscode/codicons": "0.0.36", "billboard.js": "3.14.3", @@ -21491,8 +21503,10 @@ "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": "16.14.0", + "react-dom": "16.14.0", + "react-next": "npm:react@19.0.0", + "react-dom-next": "npm:react-dom@19.0.0", "signal-utils": "0.21.1", "slug": "10.0.0", "sortablejs": "1.15.0" @@ -21506,7 +21520,9 @@ "@types/mocha": "10.0.10", "@types/node": "20.14.15", "@types/react": "17.0.83", + "@types/react-next": "npm:@types/react@19.0.6", "@types/react-dom": "17.0.25", + "@types/react-dom-next": "npm:@types/react-dom@19.0.3", "@types/sinon": "17.0.4", "@types/slug": "5.0.9", "@types/sortablejs": "1.15.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6be9ae0cffa96..46f8486cccea8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@gitkraken/gitkraken-components': specifier: 10.7.0 version: 10.7.0 + '@gitkraken/gitkraken-components-next': + specifier: npm:@gitkraken/gitkraken-components@11.0.0-vnext.3 + version: '@gitkraken/gitkraken-components@11.0.0-vnext.3(@types/react@17.0.83)(react@16.14.0)' '@gitkraken/provider-apis': specifier: 0.26.2 version: 0.26.2(encoding@0.1.13) @@ -65,6 +68,9 @@ importers: '@opentelemetry/semantic-conventions': specifier: 1.30.0 version: 1.30.0 + '@r2wc/react-to-web-component': + specifier: ^2.0.4 + version: 2.0.4(react-dom@16.14.0(react@16.14.0))(react@16.14.0) '@shoelace-style/shoelace': specifier: 2.20.0 version: 2.20.0(@floating-ui/utils@0.2.9)(@types/react@17.0.83) @@ -96,11 +102,17 @@ importers: specifier: 1.0.1 version: 1.0.1 react: - specifier: 16.8.4 - version: 16.8.4 + specifier: 16.14.0 + version: 16.14.0 react-dom: - specifier: 16.8.4 - version: 16.8.4(react@16.8.4) + specifier: 16.14.0 + version: 16.14.0(react@16.14.0) + react-dom-next: + specifier: npm:react-dom@19.0.0 + version: react-dom@19.0.0(react@16.14.0) + react-next: + specifier: npm:react@19.0.0 + version: react@19.0.0 signal-utils: specifier: 0.21.1 version: 0.21.1(signal-polyfill@0.2.2) @@ -113,7 +125,7 @@ importers: devDependencies: '@eamodio/eslint-lite-webpack-plugin': specifier: 0.2.0 - version: 0.2.0(@swc/core@1.11.9)(esbuild@0.25.1)(eslint@9.22.0(jiti@2.4.0))(webpack-cli@6.0.1)(webpack@5.98.0) + version: 0.2.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(eslint@9.22.0(jiti@2.4.0))(webpack-cli@6.0.1)(webpack@5.98.0) '@eslint/js': specifier: 9.22.0 version: 9.22.0 @@ -122,7 +134,7 @@ importers: version: 1.51.0 '@swc/core': specifier: 1.11.9 - version: 1.11.9 + version: 1.11.9(@swc/helpers@0.5.15) '@twbs/fantasticon': specifier: 3.1.0 version: 3.1.0 @@ -138,6 +150,12 @@ importers: '@types/react-dom': specifier: 17.0.25 version: 17.0.25 + '@types/react-dom-next': + specifier: npm:@types/react-dom@19.0.3 + version: '@types/react-dom@19.0.3(@types/react@17.0.83)' + '@types/react-next': + specifier: npm:@types/react@19.0.6 + version: '@types/react@19.0.6' '@types/sinon': specifier: 17.0.4 version: 17.0.4 @@ -278,7 +296,7 @@ importers: version: 3.3.2 terser-webpack-plugin: specifier: 5.3.14 - version: 5.3.14(@swc/core@1.11.9)(esbuild@0.25.1)(webpack@5.98.0) + version: 5.3.14(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack@5.98.0) ts-loader: specifier: 9.5.2 version: 9.5.2(typescript@5.8.2)(webpack@5.98.0) @@ -290,7 +308,7 @@ importers: version: 8.26.1(eslint@9.22.0(jiti@2.4.0))(typescript@5.8.2) webpack: specifier: 5.98.0 - version: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + version: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) webpack-bundle-analyzer: specifier: 4.10.2 version: 4.10.2 @@ -609,6 +627,11 @@ packages: '@gitkraken/gitkraken-components@10.7.0': resolution: {integrity: sha512-0oekeCgTgZNAtUFNH8eSIdOfPOVG3IUIXoaKuOBY0dRT6TLc5Q/ARyujdtWLHpdD3FC/GZv46N9IdQL4AEIwNA==} + '@gitkraken/gitkraken-components@11.0.0-vnext.3': + resolution: {integrity: sha512-tS/R3lewd7qqKm4L3v/2SY9++aZExINC3L5wN75ljY3z0Cei9uVKi7EIVeIR/Zil/apwnti7SfYY9+yS+9XzQg==} + peerDependencies: + react: 19.0.0 + '@gitkraken/provider-apis@0.26.2': resolution: {integrity: sha512-1NknF+CaEd+XOl9i+kkDF+SWJ99HvOWhZPM/eEL6FPVc3LEjYLZZHopk4Rt+cqYfLF7YWVFgkp7tgAT6tJzTfA==} engines: {node: '>= 14'} @@ -1076,6 +1099,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==} @@ -1106,6 +1132,37 @@ 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'} + 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.4': + resolution: {integrity: sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==} + peerDependencies: + react: '>=16.14.0' + react-dom: '>=16.14.0' + '@shoelace-style/animations@1.2.0': resolution: {integrity: sha512-avvo1xxkLbv2dgtabdewBbqcJfV0e0zCwFqkPMnHFGbJbBHorRFfMAHh1NG9ymmXn0jW95ibUVH03E1NYXD6Gw==} @@ -1203,6 +1260,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.19': resolution: {integrity: sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==} @@ -1282,9 +1342,22 @@ packages: '@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-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + '@types/react@17.0.83': resolution: {integrity: sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw==} + '@types/react@19.0.6': + resolution: {integrity: sha512-gIlMztcTeDgXCUj0vCBOqEuSEhX//63fW9SZtCJ+agxoQTOklwDfiEMlTWn4mR/C/UK5VHlpwsCsOyf7/hc4lw==} + '@types/scheduler@0.16.8': resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} @@ -1306,6 +1379,9 @@ packages: '@types/vscode@1.92.0': resolution: {integrity: sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==} + '@types/warning@3.0.3': + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} + '@types/webpack@5.28.5': resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} @@ -2374,6 +2450,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} @@ -4512,6 +4592,12 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + re-resizable@6.10.3: + resolution: {integrity: sha512-zvWb7X3RJMA4cuSrqoxgs3KR+D+pEXnGrD2FAD6BMYAULnZsSF4b7AOVyG6pC3VVNVOtlagGDCDmZSwWLjjBBw==} + peerDependencies: + 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 + re-resizable@6.9.11: resolution: {integrity: sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ==} peerDependencies: @@ -4524,11 +4610,31 @@ packages: react: ^0.14.9 || >=15.3.0 react-dom: ^0.14.9 || >=15.3.0 + react-bootstrap@2.10.7: + resolution: {integrity: sha512-w6mWb3uytB5A18S2oTZpYghcOUK30neMBBvZ/bEfA+WIF2dF4OGqjzoFVMpVXBjtyf92gkmRToHlddiMAVhQqQ==} + peerDependencies: + '@types/react': '>=16.14.8' + react: '>=16.14.0' + react-dom: '>=16.14.0' + peerDependenciesMeta: + '@types/react': + optional: true + + react-dom@16.14.0: + resolution: {integrity: sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==} + peerDependencies: + react: ^16.14.0 + react-dom@16.8.4: resolution: {integrity: sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ==} peerDependencies: react: ^16.0.0 + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + peerDependencies: + react: ^19.0.0 + react-dragula@1.1.17: resolution: {integrity: sha512-gJdY190sPWAyV8jz79vyK9SGk97bVOHjUguVNIYIEVosvt27HLxnbJo4qiuEkb/nAuGY13Im2CHup92fUyO3fw==} @@ -4544,6 +4650,12 @@ packages: react: ^15.5.x || ^16.x || ^17.x || ^18.x react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + 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: @@ -4561,10 +4673,24 @@ packages: react: '>=15.0.0' react-dom: '>=15.0.0' + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@16.14.0: + resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} + engines: {node: '>=0.10.0'} + react@16.8.4: resolution: {integrity: sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg==} engines: {node: '>=0.10.0'} + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + engines: {node: '>=0.10.0'} + read-installed-packages@2.0.1: resolution: {integrity: sha512-t+fJOFOYaZIjBpTVxiV8Mkt7yQyy4E6MSrrnt5FmPd4enYvpU/9DYGirDmN1XQwkfeuWIhM/iu0t2rm6iSr0CA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4881,6 +5007,12 @@ packages: scheduler@0.13.6: resolution: {integrity: sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==} + scheduler@0.19.1: + resolution: {integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==} + + 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==} engines: {node: '>= 10.13.0'} @@ -5396,6 +5528,16 @@ packages: peerDependencies: react: '>=15.0.0' + 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==} @@ -5651,6 +5793,17 @@ snapshots: react-dom: 16.8.4(react@16.8.4) react-lifecycles-compat: 3.0.4 + '@axosoft/react-virtualized@9.22.3-gitkraken.3(react-dom@19.0.0(react@16.14.0))(react@16.14.0)': + dependencies: + '@babel/runtime': 7.26.10 + clsx: 1.2.1 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 16.14.0 + react-dom: 19.0.0(react@16.14.0) + react-lifecycles-compat: 3.0.4 + '@azure/abort-controller@2.1.2': dependencies: tslib: 2.8.1 @@ -5757,14 +5910,14 @@ snapshots: '@discoveryjs/json-ext@0.6.3': {} - '@eamodio/eslint-lite-webpack-plugin@0.2.0(@swc/core@1.11.9)(esbuild@0.25.1)(eslint@9.22.0(jiti@2.4.0))(webpack-cli@6.0.1)(webpack@5.98.0)': + '@eamodio/eslint-lite-webpack-plugin@0.2.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(eslint@9.22.0(jiti@2.4.0))(webpack-cli@6.0.1)(webpack@5.98.0)': dependencies: '@types/eslint': 9.6.1 - '@types/webpack': 5.28.5(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + '@types/webpack': 5.28.5(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) eslint: 9.22.0(jiti@2.4.0) fast-glob: 3.3.3 minimatch: 10.0.1 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -5930,6 +6083,19 @@ snapshots: react-dragula: 1.1.17 react-onclickoutside: 6.13.0(react-dom@16.8.4(react@16.8.4))(react@16.8.4) + '@gitkraken/gitkraken-components@11.0.0-vnext.3(@types/react@17.0.83)(react@16.14.0)': + dependencies: + '@axosoft/react-virtualized': 9.22.3-gitkraken.3(react-dom@19.0.0(react@16.14.0))(react@16.14.0) + classnames: 2.5.1 + re-resizable: 6.10.3(react-dom@19.0.0(react@16.14.0))(react@16.14.0) + react: 16.14.0 + react-bootstrap: 2.10.7(@types/react@17.0.83)(react-dom@19.0.0(react@16.14.0))(react@16.14.0) + react-dom: 19.0.0(react@16.14.0) + react-dragula: 1.1.17 + react-onclickoutside: 6.13.1(react-dom@19.0.0(react@16.14.0))(react@16.14.0) + transitivePeerDependencies: + - '@types/react' + '@gitkraken/provider-apis@0.26.2(encoding@0.1.13)': dependencies: js-base64: 3.7.5 @@ -6349,6 +6515,8 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@popperjs/core@2.11.8': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -6372,6 +6540,43 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@r2wc/core@1.2.0': {} + + '@r2wc/react-to-web-component@2.0.4(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': + dependencies: + '@r2wc/core': 1.2.0 + react: 16.14.0 + react-dom: 16.14.0(react@16.14.0) + + '@react-aria/ssr@3.9.7(react@16.14.0)': + dependencies: + '@swc/helpers': 0.5.15 + react: 16.14.0 + + '@restart/hooks@0.4.16(react@16.14.0)': + dependencies: + dequal: 2.0.3 + react: 16.14.0 + + '@restart/hooks@0.5.1(react@16.14.0)': + dependencies: + dequal: 2.0.3 + react: 16.14.0 + + '@restart/ui@1.9.4(react-dom@19.0.0(react@16.14.0))(react@16.14.0)': + dependencies: + '@babel/runtime': 7.26.10 + '@popperjs/core': 2.11.8 + '@react-aria/ssr': 3.9.7(react@16.14.0) + '@restart/hooks': 0.5.1(react@16.14.0) + '@types/warning': 3.0.3 + dequal: 2.0.3 + dom-helpers: 5.2.1 + react: 16.14.0 + react-dom: 19.0.0(react@16.14.0) + uncontrollable: 8.0.4(react@16.14.0) + warning: 4.0.3 + '@shoelace-style/animations@1.2.0': {} '@shoelace-style/localize@3.2.1': {} @@ -6438,7 +6643,7 @@ snapshots: '@swc/core-win32-x64-msvc@1.11.9': optional: true - '@swc/core@1.11.9': + '@swc/core@1.11.9(@swc/helpers@0.5.15)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.19 @@ -6453,9 +6658,14 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.11.9 '@swc/core-win32-ia32-msvc': 1.11.9 '@swc/core-win32-x64-msvc': 1.11.9 + '@swc/helpers': 0.5.15 '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + '@swc/types@0.1.19': dependencies: '@swc/counter': 0.1.3 @@ -6547,12 +6757,24 @@ snapshots: dependencies: '@types/react': 17.0.83 + '@types/react-dom@19.0.3(@types/react@17.0.83)': + dependencies: + '@types/react': 17.0.83 + + '@types/react-transition-group@4.4.12(@types/react@17.0.83)': + dependencies: + '@types/react': 17.0.83 + '@types/react@17.0.83': dependencies: '@types/prop-types': 15.7.14 '@types/scheduler': 0.16.8 csstype: 3.1.3 + '@types/react@19.0.6': + dependencies: + csstype: 3.1.3 + '@types/scheduler@0.16.8': {} '@types/sinon@17.0.4': @@ -6569,11 +6791,13 @@ snapshots: '@types/vscode@1.92.0': {} - '@types/webpack@5.28.5(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1)': + '@types/warning@3.0.3': {} + + '@types/webpack@5.28.5(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1)': dependencies: '@types/node': 20.14.15 tapable: 2.2.1 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -6856,17 +7080,17 @@ snapshots: '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.98.0)': dependencies: - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-bundle-analyzer@4.10.2)(webpack@5.98.0) '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.98.0)': dependencies: - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-bundle-analyzer@4.10.2)(webpack@5.98.0) '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.98.0)': dependencies: - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-bundle-analyzer@4.10.2)(webpack@5.98.0) '@xmldom/xmldom@0.7.13': {} @@ -7344,7 +7568,7 @@ snapshots: circular-dependency-plugin@5.2.2(webpack@5.98.0): dependencies: - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) classnames@2.5.1: {} @@ -7357,7 +7581,7 @@ snapshots: clean-webpack-plugin@4.0.0(webpack@5.98.0): dependencies: del: 4.1.1 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) cli-cursor@4.0.0: dependencies: @@ -7470,7 +7694,7 @@ snapshots: schema-utils: 4.3.0 serialize-javascript: 6.0.2 tinyglobby: 0.2.12 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) core-js@2.6.12: {} @@ -7500,7 +7724,7 @@ snapshots: cheerio: 1.0.0-rc.12 html-webpack-plugin: 5.6.3(webpack@5.98.0) lodash: 4.17.21 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) css-declaration-sorter@7.2.0(postcss@8.5.3): dependencies: @@ -7517,7 +7741,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.1 optionalDependencies: - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) css-minimizer-webpack-plugin@7.0.2(esbuild@0.25.1)(webpack@5.98.0): dependencies: @@ -7527,7 +7751,7 @@ snapshots: postcss: 8.5.3 schema-utils: 4.3.0 serialize-javascript: 6.0.2 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) optionalDependencies: esbuild: 0.25.1 @@ -7811,6 +8035,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-libc@1.0.3: @@ -8044,7 +8270,7 @@ snapshots: esbuild: 0.25.1 get-tsconfig: 4.10.0 loader-utils: 2.0.4 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) webpack-sources: 1.4.3 esbuild-node-externals@1.18.0(esbuild@0.25.1): @@ -8111,7 +8337,7 @@ snapshots: optionalDependencies: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.0))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.0)) eslint-plugin-import-x: 4.6.1(eslint@9.22.0(jiti@2.4.0))(typescript@5.8.2) - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-bundle-analyzer@4.10.2)(webpack@5.98.0) webpack-merge: 6.0.1 @@ -8368,7 +8594,7 @@ snapshots: semver: 7.7.1 tapable: 2.2.1 typescript: 5.8.2 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) form-data@4.0.2: dependencies: @@ -8611,7 +8837,7 @@ snapshots: dependencies: html-minifier-terser: 7.2.0 parse5: 7.2.1 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) html-minifier-terser@6.1.0: dependencies: @@ -8641,7 +8867,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) htmlparser2@6.1.0: dependencies: @@ -8736,7 +8962,7 @@ snapshots: dependencies: schema-utils: 4.3.0 serialize-javascript: 6.0.2 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) optionalDependencies: sharp: 0.33.5 svgo: 3.3.2 @@ -9426,7 +9652,7 @@ snapshots: dependencies: schema-utils: 4.3.0 tapable: 2.2.1 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) minimatch@10.0.1: dependencies: @@ -10158,6 +10384,12 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + prop-types-extra@1.1.1(react@16.14.0): + dependencies: + react: 16.14.0 + react-is: 16.13.1 + warning: 4.0.3 + prop-types-extra@1.1.1(react@16.8.4): dependencies: react: 16.8.4 @@ -10227,6 +10459,11 @@ snapshots: strip-json-comments: 2.0.1 optional: true + re-resizable@6.10.3(react-dom@19.0.0(react@16.14.0))(react@16.14.0): + dependencies: + react: 16.14.0 + react-dom: 19.0.0(react@16.14.0) + re-resizable@6.9.11(react-dom@16.8.4(react@16.8.4))(react@16.8.4): dependencies: react: 16.8.4 @@ -10249,6 +10486,34 @@ snapshots: uncontrollable: 5.1.0(react@16.8.4) warning: 3.0.0 + react-bootstrap@2.10.7(@types/react@17.0.83)(react-dom@19.0.0(react@16.14.0))(react@16.14.0): + dependencies: + '@babel/runtime': 7.26.10 + '@restart/hooks': 0.4.16(react@16.14.0) + '@restart/ui': 1.9.4(react-dom@19.0.0(react@16.14.0))(react@16.14.0) + '@types/prop-types': 15.7.14 + '@types/react-transition-group': 4.4.12(@types/react@17.0.83) + classnames: 2.5.1 + dom-helpers: 5.2.1 + invariant: 2.2.4 + prop-types: 15.8.1 + prop-types-extra: 1.1.1(react@16.14.0) + react: 16.14.0 + react-dom: 19.0.0(react@16.14.0) + react-transition-group: 4.4.5(react-dom@19.0.0(react@16.14.0))(react@16.14.0) + uncontrollable: 7.2.1(react@16.14.0) + warning: 4.0.3 + optionalDependencies: + '@types/react': 17.0.83 + + react-dom@16.14.0(react@16.14.0): + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + prop-types: 15.8.1 + react: 16.14.0 + scheduler: 0.19.1 + react-dom@16.8.4(react@16.8.4): dependencies: loose-envify: 1.4.0 @@ -10257,6 +10522,11 @@ snapshots: react: 16.8.4 scheduler: 0.13.6 + react-dom@19.0.0(react@16.14.0): + dependencies: + react: 16.14.0 + scheduler: 0.25.0 + react-dragula@1.1.17: dependencies: atoa: 1.0.0 @@ -10271,6 +10541,11 @@ snapshots: react: 16.8.4 react-dom: 16.8.4(react@16.8.4) + react-onclickoutside@6.13.1(react-dom@19.0.0(react@16.14.0))(react@16.14.0): + dependencies: + react: 16.14.0 + react-dom: 19.0.0(react@16.14.0) + react-overlays@0.8.3(react-dom@16.8.4(react@16.8.4))(react@16.8.4): dependencies: classnames: 2.5.1 @@ -10296,6 +10571,21 @@ snapshots: react-dom: 16.8.4(react@16.8.4) react-lifecycles-compat: 3.0.4 + react-transition-group@4.4.5(react-dom@19.0.0(react@16.14.0))(react@16.14.0): + dependencies: + '@babel/runtime': 7.26.10 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 16.14.0 + react-dom: 19.0.0(react@16.14.0) + + react@16.14.0: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + prop-types: 15.8.1 + react@16.8.4: dependencies: loose-envify: 1.4.0 @@ -10303,6 +10593,8 @@ snapshots: prop-types: 15.8.1 scheduler: 0.13.6 + react@19.0.0: {} + read-installed-packages@2.0.1: dependencies: '@npmcli/fs': 3.1.1 @@ -10581,7 +10873,7 @@ snapshots: optionalDependencies: sass: 1.85.1 sass-embedded: 1.77.8 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) sass@1.85.1: dependencies: @@ -10598,6 +10890,13 @@ snapshots: loose-envify: 1.4.0 object-assign: 4.1.1 + scheduler@0.19.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + scheduler@0.25.0: {} + schema-utils@3.3.0: dependencies: '@types/json-schema': 7.0.15 @@ -11019,16 +11318,16 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - terser-webpack-plugin@5.3.14(@swc/core@1.11.9)(esbuild@0.25.1)(webpack@5.98.0): + terser-webpack-plugin@5.3.14(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack@5.98.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.0 serialize-javascript: 6.0.2 terser: 5.39.0 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) optionalDependencies: - '@swc/core': 1.11.9 + '@swc/core': 1.11.9(@swc/helpers@0.5.15) esbuild: 0.25.1 terser@5.39.0: @@ -11088,7 +11387,7 @@ snapshots: semver: 7.7.1 source-map: 0.7.4 typescript: 5.8.2 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) tsconfig-paths@3.15.0: dependencies: @@ -11218,6 +11517,18 @@ snapshots: invariant: 2.2.4 react: 16.8.4 + uncontrollable@7.2.1(react@16.14.0): + dependencies: + '@babel/runtime': 7.26.10 + '@types/react': 19.0.6 + invariant: 2.2.4 + react: 16.14.0 + react-lifecycles-compat: 3.0.4 + + uncontrollable@8.0.4(react@16.14.0): + dependencies: + react: 16.14.0 + underscore@1.13.7: {} undici-types@5.26.5: {} @@ -11316,7 +11627,7 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1) + webpack: 5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1) webpack-merge: 6.0.1 optionalDependencies: webpack-bundle-analyzer: 4.10.2 @@ -11340,7 +11651,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.98.0(@swc/core@1.11.9)(esbuild@0.25.1)(webpack-cli@6.0.1): + webpack@5.98.0(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack-cli@6.0.1): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.6 @@ -11362,7 +11673,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.14(@swc/core@1.11.9)(esbuild@0.25.1)(webpack@5.98.0) + terser-webpack-plugin: 5.3.14(@swc/core@1.11.9(@swc/helpers@0.5.15))(esbuild@0.25.1)(webpack@5.98.0) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: diff --git a/src/config.ts b/src/config.ts index 827858799eaeb..574e4fbd1d4f5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -373,6 +373,11 @@ export interface GraphConfig { readonly dateStyle: DateStyle | null; readonly defaultItemLimit: number; readonly dimMergeCommits: boolean; + readonly experimental: { + readonly renderer: { + readonly enabled: boolean; + }; + }; readonly highlightRowsOnRefHover: boolean; readonly issues: { readonly enabled: boolean; diff --git a/src/system/decorators/debounce.ts b/src/system/decorators/debounce.ts new file mode 100644 index 0000000000000..171b9cc3e2e81 --- /dev/null +++ b/src/system/decorators/debounce.ts @@ -0,0 +1,16 @@ +import { debounce as debounceFunction } from '../function/debounce'; + +export function debounce 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 = debounceFunction(targetFields.value, delay); + return { + ...targetFields, + // @ts-expect-error Deferrable to T is safe + value: debounced as T, + }; + }; +} diff --git a/src/webviews/apps/plus/graph-next/actions/gitActionsButtons.ts b/src/webviews/apps/plus/graph-next/actions/gitActionsButtons.ts new file mode 100644 index 0000000000000..a12b3f1e383a9 --- /dev/null +++ b/src/webviews/apps/plus/graph-next/actions/gitActionsButtons.ts @@ -0,0 +1,277 @@ +import { css, html, LitElement, nothing } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { fromNow } from '../../../../../system/date'; +import { pluralize } from '../../../../../system/string'; +import { createWebviewCommandLink } from '../../../../../system/webview'; +import type { BranchState, State } from '../../../../plus/graph/protocol'; +import { inlineCode } from '../../../shared/components/styles/lit/base.css'; +import { actionButton, linkBase, ruleBase } from '../styles/graph.css'; +import '../../../shared/components/code-icon'; +import '../../../shared/components/overlays/tooltip'; + +@customElement('gl-git-actions-buttons') +export class GitActionsButtons extends LitElement { + @property({ type: Object }) + branchState?: BranchState; + + @property({ type: String }) + branchName?: string; + + @property({ type: Object }) + lastFetched?: Date; + + @property({ type: Object }) + state!: State; + + private get fetchedText() { + if (!this.lastFetched) return undefined; + + let lastFetchedDate: Date; + if (typeof this.lastFetched === 'string') { + lastFetchedDate = new Date(this.lastFetched); + } else { + lastFetchedDate = this.lastFetched; + } + + return lastFetchedDate.getTime() !== 0 ? fromNow(lastFetchedDate) : undefined; + } + + override render() { + return html` + + + `; + } +} + +@customElement('gl-fetch-button') +export class GlFetchButton extends LitElement { + static override styles = [linkBase, inlineCode, actionButton, ruleBase]; + + @property({ type: Object }) + state!: State; + + @property({ type: String }) + fetchedText?: string; + + @property({ type: Object }) + branchState?: BranchState; + + private get upstream() { + return this.branchState?.upstream + ? html`${this.branchState.upstream}` + : 'remote'; + } + + override render() { + return html` + + + + Fetch + ${this.fetchedText ? html`(${this.fetchedText})` : ''} + + + Fetch from ${this.upstream} + ${this.branchState?.provider?.name ? html` on ${this.branchState.provider.name}` : ''} + ${this.fetchedText + ? html` +
+ Last fetched ${this.fetchedText} + ` + : nothing} +
+
+ `; + } +} + +@customElement('gl-push-pull-button') +export class PushPullButton extends LitElement { + static override styles = [ + linkBase, + inlineCode, + actionButton, + ruleBase, + css` + .pill { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 0.5rem; + font-size: 1rem; + font-weight: 500; + line-height: 1.2; + text-transform: uppercase; + color: var(--vscode-foreground); + background-color: var(--vscode-editorWidget-background); + } + .pill code-icon { + font-size: inherit !important; + line-height: inherit !important; + } + `, + ]; + + @property({ type: Object }) + branchState?: BranchState; + + @property({ type: Object }) + state!: State; + + @property({ type: String }) + fetchedText?: string; + + @property({ type: String }) + branchName?: string; + + private get isBehind(): boolean { + return (this.branchState?.behind ?? 0) > 0; + } + + private get isAhead(): boolean { + return (this.branchState?.ahead ?? 0) > 0; + } + + private get upstream() { + return this.branchState?.upstream + ? html`${this.branchState.upstream}` + : 'remote'; + } + + private renderBranchPrefix() { + return html`${this.branchName} is`; + } + + private renderTooltipContent(action: 'pull' | 'push') { + if (!this.branchState) return nothing; + + const providerSuffix = this.branchState.provider?.name ? html` on ${this.branchState.provider.name}` : ''; + + if (action === 'pull') { + const mainContent = html`Pull ${pluralize('commit', this.branchState.behind)} from + ${this.upstream}${providerSuffix}`; + + if (this.isAhead) { + return html` + ${mainContent} +
+ ${this.renderBranchPrefix()} ${pluralize('commit', this.branchState.behind)} behind and + ${pluralize('commit', this.branchState.ahead)} ahead of ${this.upstream}${providerSuffix} + `; + } + + return html` + ${mainContent} +
+ ${this.renderBranchPrefix()} ${pluralize('commit', this.branchState.behind)} behind + ${this.upstream}${providerSuffix} + `; + } + + return html` + Push ${pluralize('commit', this.branchState.ahead)} to ${this.upstream}${providerSuffix} +
+ ${this.renderBranchPrefix()} ${pluralize('commit', this.branchState.ahead)} ahead of ${this.upstream} + `; + } + + override render() { + if (!this.branchState || (!this.isAhead && !this.isBehind)) { + return nothing; + } + + const action = this.isBehind ? 'pull' : 'push'; + const icon = this.isBehind ? 'repo-pull' : 'repo-push'; + const label = this.isBehind ? 'Pull' : 'Push'; + + return html` + + + + ${label} + + + ${this.isBehind + ? html` + + ${this.branchState.behind} + + + ` + : ''} + ${this.isAhead + ? html` + + ${this.isBehind ? html`  ` : ''} ${this.branchState.ahead} + + + ` + : ''} + + + +
+ ${this.renderTooltipContent(action)} + ${this.fetchedText + ? html` +
+ Last fetched ${this.fetchedText} + ` + : ''} +
+
+ ${this.isAhead && this.isBehind + ? html` + + + + + + Force Push ${pluralize('commit', this.branchState?.ahead)} to ${this.upstream} + ${this.branchState?.provider?.name ? html` on ${this.branchState.provider.name}` : ''} + + + ` + : ''} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'gl-git-actions-buttons': GitActionsButtons; + 'gl-fetch-button': GlFetchButton; + 'gl-push-pull-button': PushPullButton; + } +} diff --git a/src/webviews/apps/plus/graph-next/context.ts b/src/webviews/apps/plus/graph-next/context.ts new file mode 100644 index 0000000000000..afe1f15d3c0f2 --- /dev/null +++ b/src/webviews/apps/plus/graph-next/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-next/gate.ts b/src/webviews/apps/plus/graph-next/gate.ts new file mode 100644 index 0000000000000..7ed3663a942ed --- /dev/null +++ b/src/webviews/apps/plus/graph-next/gate.ts @@ -0,0 +1,58 @@ +import { consume } from '@lit/context'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { createWebviewCommandLink } from '../../../../system/webview'; +import type { State } from '../../../plus/graph/protocol'; +import { GlElement } from '../../shared/components/element'; +import { stateContext } from './context'; +import '../../shared/components/feature-badge'; +import '../../shared/components/feature-gate'; + +@customElement('gl-graph-gate') +export class GlGraphGate extends GlElement { + static override styles = css` + gl-feature-gate gl-feature-badge { + vertical-align: super; + margin-left: 0.4rem; + margin-right: 0.4rem; + } + `; + + @consume({ context: stateContext, subscribe: true }) + @state() + state!: State; + + 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. +

+
`; + } +} diff --git a/src/webviews/apps/plus/graph-next/graph-app.ts b/src/webviews/apps/plus/graph-next/graph-app.ts new file mode 100644 index 0000000000000..96d3c52c57497 --- /dev/null +++ b/src/webviews/apps/plus/graph-next/graph-app.ts @@ -0,0 +1,129 @@ +import { consume } from '@lit/context'; +import { SignalWatcher } from '@lit-labs/signals'; +import { html, LitElement } from 'lit'; +import { customElement, query } from 'lit/decorators.js'; +import { when } from 'lit/directives/when.js'; +import type { CustomEventType } from '../../shared/components/element'; +import { emitTelemetrySentEvent } from '../../shared/telemetry'; +import type { GraphMinimapDaySelectedEventDetail } from '../graph/minimap/minimap'; +import type { GlGraphMinimapContainer } from '../graph/minimap/minimap-container'; +import { stateContext } from './context'; +import type { GLGraphWrapper } from './graph-wrapper/graph-wrapper'; +import { graphStateContext } from './stateProvider'; +import '../graph/minimap/minimap-container'; +import './graph-wrapper/graph-wrapper'; +import './sidebar/sidebar'; +import './graph-header'; +import './gate'; + +@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') + 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) => { + return Math.abs(curr.date - date) < Math.abs(prev.date - date) ? curr : prev; + }); + sha = closest.sha; + } + + this.graphWrapper.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.graphWrapper.resetHover(); + } + + override render() { + return html` +
+ +
+ ${when(!this.state.allowed, () => html``)} +
+
+ ${when( + this.state.config?.minimap !== false, + () => html` + + `, + )} + ${when(this.state.config?.sidebar, () => html``)} + +
+ +
+
+
+ `; + } +} diff --git a/src/webviews/apps/plus/graph-next/graph-header.ts b/src/webviews/apps/plus/graph-next/graph-header.ts new file mode 100644 index 0000000000000..e3f9691de0cd1 --- /dev/null +++ b/src/webviews/apps/plus/graph-next/graph-header.ts @@ -0,0 +1,1164 @@ +import type { GraphRefOptData } from '@gitkraken/gitkraken-components'; +import { consume } from '@lit/context'; +import { SignalWatcher } from '@lit-labs/signals'; +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 { debounce } from '../../../../system/decorators/debounce'; +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 type { CustomEventType } from '../../shared/components/element'; +import type { RadioGroup } from '../../shared/components/radio/radio-group'; +import type { GlSearchBox } from '../../shared/components/search/search-box'; +import type { SearchNavigationEventDetail } from '../../shared/components/search/search-input'; +import { inlineCode } from '../../shared/components/styles/lit/base.css'; +import { ipcContext } from '../../shared/contexts/ipc'; +import type { TelemetryContext } from '../../shared/contexts/telemetry'; +import { telemetryContext } from '../../shared/contexts/telemetry'; +import { emitTelemetrySentEvent } from '../../shared/telemetry'; +import { stateContext } from './context'; +import { graphStateContext } from './stateProvider'; +import { actionButton, linkBase, ruleBase } from './styles/graph.css'; +import { graphHeaderControlStyles, progressStyles, repoHeaderStyles, titlebarStyles } from './styles/header.css'; +import '@shoelace-style/shoelace/dist/components/option/option.js'; +import '@shoelace-style/shoelace/dist/components/select/select.js'; +import '../../shared/components/button'; +import '../../shared/components/checkbox/checkbox'; +import '../../shared/components/code-icon'; +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 '../../shared/components/rich/issue-pull-request'; +import '../../shared/components/search/search-box'; +import '../shared/components/merge-rebase-status'; +import './actions/gitActionsButtons'; + +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) { + static override styles = [ + inlineCode, + linkBase, + ruleBase, + actionButton, + titlebarStyles, + repoHeaderStyles, + graphHeaderControlStyles, + progressStyles, + ]; + + // FIXME: remove 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); + } + + @debounce(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 }); + } + } + } + + @debounce(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: { 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: { 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 + + +
+
+
+
+
+ `, + )} +
+
+
+
`; + } +} diff --git a/src/webviews/apps/plus/graph-next/graph-next.html b/src/webviews/apps/plus/graph-next/graph-next.html new file mode 100644 index 0000000000000..c94b6f71cb600 --- /dev/null +++ b/src/webviews/apps/plus/graph-next/graph-next.html @@ -0,0 +1,30 @@ + + + + + + + + + + + #{endOfBody} + + diff --git a/src/webviews/apps/plus/graph-next/graph-wrapper/graph-wrapper.react.tsx b/src/webviews/apps/plus/graph-next/graph-wrapper/graph-wrapper.react.tsx new file mode 100644 index 0000000000000..5b4fcd2a22071 --- /dev/null +++ b/src/webviews/apps/plus/graph-next/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.nativeEvent.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-next/graph-wrapper/graph-wrapper.ts b/src/webviews/apps/plus/graph-next/graph-wrapper/graph-wrapper.ts new file mode 100644 index 0000000000000..c9cfe1664fbbf --- /dev/null +++ b/src/webviews/apps/plus/graph-next/graph-wrapper/graph-wrapper.ts @@ -0,0 +1,322 @@ +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 { ipcContext } from '../../../shared/contexts/ipc'; +import type { TelemetryContext } from '../../../shared/contexts/telemetry'; +import { telemetryContext } from '../../../shared/contexts/telemetry'; +import type { GlGraphHover } from '../../graph/hover/graphHover'; +import { stateContext } from '../context'; +import { graphStateContext } from '../stateProvider'; +import { GraphWrapperReact } from './graph-wrapper.react'; +import '../../graph/hover/graphHover'; + +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; + + resetHover() { + this.graphHover.reset(); + } + + private handleRowContextMenu() { + this.graphHover.hide(); + } + + override render() { + return html` { + // eslint-disable-next-line lit/no-this-assign-in-render + 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-next/graph.scss b/src/webviews/apps/plus/graph-next/graph.scss new file mode 100644 index 0000000000000..69c57063ab4fc --- /dev/null +++ b/src/webviews/apps/plus/graph-next/graph.scss @@ -0,0 +1,1438 @@ +@use '../../shared/styles/theme'; +@use '../../shared/styles/utils'; +@use '../../shared/styles/icons/utils' as iconUtils; +@import '../../shared/base'; +@import '../../../../../node_modules/@gitkraken/gitkraken-components/dist/styles.css'; + +@mixin focusStyles() { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +@include utils.dark-theme { + --popover-bg: var(--color-background--lighten-15); + --titlebar-bg: var(--color-background--lighten-075); +} + +@include utils.light-theme { + --popover-bg: var(--color-background--darken-15); + --titlebar-bg: var(--color-background--darken-075); +} + +:root { + --titlebar-fg: var(--color-foreground--65); + --color-graph-contrast-border: var(--vscode-list-focusOutline); + --color-graph-selected-row: var(--vscode-list-activeSelectionBackground); + --color-graph-hover-row: var(--vscode-list-hoverBackground); + --color-graph-text-selected-row: var(--vscode-list-activeSelectionForeground); + --color-graph-text-dimmed-selected: color-mix( + in srgb, + transparent 50%, + var(--vscode-list-activeSelectionForeground) + ); + --color-graph-text-selected: var(--vscode-editor-foreground, var(--vscode-foreground)); + --color-graph-text-dimmed: color-mix(in srgb, transparent 80%, var(--color-graph-text-selected)); + --color-graph-actionbar-selectedBackground: var(--vscode-toolbar-hoverBackground); + + --color-graph-text-hovered: var(--vscode-list-hoverForeground); + --color-graph-text-normal: color-mix(in srgb, transparent 15%, var(--color-graph-text-selected)); + --color-graph-text-secondary: color-mix(in srgb, transparent 35%, var(--color-graph-text-selected)); + --color-graph-text-disabled: color-mix(in srgb, transparent 50%, var(--color-graph-text-selected)); + + --color-graph-stats-added: var(--vscode-gitlens-graphChangesColumnAddedColor); + --color-graph-stats-deleted: var(--vscode-gitlens-graphChangesColumnDeletedColor); + --color-graph-stats-files: var(--vscode-gitDecoration-modifiedResourceForeground); + + --color-graph-minimap-line0: var(--vscode-progressBar-background); + --color-graph-minimap-focusLine: var(--vscode-foreground); + --color-graph-minimap-visibleAreaBackground: var(--vscode-scrollbarSlider-background); + + --color-graph-minimap-marker-head: var(--vscode-gitlens-graphMinimapMarkerHeadColor); + --color-graph-scroll-marker-head: var(--vscode-gitlens-graphScrollMarkerHeadColor); + --color-graph-minimap-marker-upstream: var(--vscode-gitlens-graphMinimapMarkerUpstreamColor); + --color-graph-scroll-marker-upstream: var(--vscode-gitlens-graphScrollMarkerUpstreamColor); + --color-graph-minimap-marker-highlights: var(--vscode-gitlens-graphMinimapMarkerHighlightsColor); + --color-graph-scroll-marker-highlights: var(--vscode-gitlens-graphScrollMarkerHighlightsColor); + --color-graph-minimap-marker-local-branches: var(--vscode-gitlens-graphMinimapMarkerLocalBranchesColor); + --color-graph-scroll-marker-local-branches: var(--vscode-gitlens-graphScrollMarkerLocalBranchesColor); + --color-graph-minimap-marker-pull-requests: var(--vscode-gitlens-graphMinimapMarkerPullRequestsColor); + --color-graph-scroll-marker-pull-requests: var(--vscode-gitlens-graphScrollMarkerPullRequestsColor); + --color-graph-minimap-marker-remote-branches: var(--vscode-gitlens-graphMinimapMarkerRemoteBranchesColor); + --color-graph-scroll-marker-remote-branches: var(--vscode-gitlens-graphScrollMarkerRemoteBranchesColor); + --color-graph-minimap-marker-stashes: var(--vscode-gitlens-graphMinimapMarkerStashesColor); + --color-graph-scroll-marker-stashes: var(--vscode-gitlens-graphScrollMarkerStashesColor); + --color-graph-minimap-marker-tags: var(--vscode-gitlens-graphMinimapMarkerTagsColor); + --color-graph-scroll-marker-tags: var(--vscode-gitlens-graphScrollMarkerTagsColor); + + --color-graph-minimap-tip-headBackground: var(--color-graph-scroll-marker-head); + --color-graph-minimap-tip-headBorder: var(--color-graph-scroll-marker-head); + --color-graph-minimap-tip-headForeground: var(--vscode-editor-foreground, var(--vscode-foreground)); + + --color-graph-minimap-tip-highlightBackground: var(--color-graph-scroll-marker-highlights); + --color-graph-minimap-tip-highlightBorder: var(--color-graph-scroll-marker-highlights); + --color-graph-minimap-tip-highlightForeground: var(--vscode-editor-foreground, var(--vscode-foreground)); + + --color-graph-minimap-tip-branchBackground: var(--color-graph-scroll-marker-local-branches); + --color-graph-minimap-tip-branchBorder: var(--color-graph-scroll-marker-local-branches); + --color-graph-minimap-tip-branchForeground: var(--vscode-editor-foreground, var(--vscode-foreground)); + + --color-graph-minimap-tip-remoteBackground: var(--color-graph-scroll-marker-remote-branches); + --color-graph-minimap-tip-remoteBorder: var(--color-graph-scroll-marker-remote-branches); + --color-graph-minimap-tip-remoteForeground: var(--vscode-editor-foreground, var(--vscode-foreground)); + + --color-graph-scroll-marker-selection: var(--vscode-editorCursor-foreground); + --color-graph-minimap-marker-selection: var(--color-graph-scroll-marker-selection); + + --color-graph-minimap-tip-stashBackground: var(--color-graph-scroll-marker-stashes); + --color-graph-minimap-tip-stashBorder: var(--color-graph-scroll-marker-stashes); + --color-graph-minimap-tip-stashForeground: var(--vscode-editor-foreground, var(--vscode-foreground)); + + --color-graph-minimap-pullRequestBackground: var(--color-graph-scroll-marker-pull-requests); + --color-graph-minimap-pullRequestBorder: var(--color-graph-scroll-marker-pull-requests); + --color-graph-minimap-pullRequestForeground: var(--vscode-editor-foreground, var(--vscode-foreground)); + + --color-graph-minimap-tip-tagBackground: var(--color-graph-scroll-marker-tags); + --color-graph-minimap-tip-tagBorder: var(--color-graph-scroll-marker-tags); + --color-graph-minimap-tip-tagForeground: var(--vscode-editor-foreground, var(--vscode-foreground)); + + --color-graph-minimap-tip-upstreamBackground: var(--color-graph-scroll-marker-upstream); + --color-graph-minimap-tip-upstreamBorder: var(--color-graph-scroll-marker-upstream); + --color-graph-minimap-tip-upstreamForeground: var(--vscode-editor-foreground, var(--vscode-foreground)); + + --graph-stats-bar-height: 40%; + --graph-stats-bar-border-radius: 3px; + + --branch-status-ahead-foreground: var(--vscode-gitlens-decorations\.branchAheadForegroundColor); + --branch-status-behind-foreground: var(--vscode-gitlens-decorations\.branchBehindForegroundColor); + --branch-status-both-foreground: var(--vscode-gitlens-decorations\.branchDivergedForegroundColor); + + --graph-column-scrollbar-thickness: 14px; +} + +@include utils.dark-theme($selectorPrefix: ':root:has(', $selectorPostfix: ')') { + --graph-theme-opacity-factor: '1'; + + --color-graph-actionbar-background: color-mix(in srgb, #fff 5%, var(--color-background)); + --color-graph-background: color-mix(in srgb, #fff 5%, var(--color-background)); + --color-graph-background2: color-mix(in srgb, #fff 10%, var(--color-background)); +} + +@include utils.light-theme($selectorPrefix: ':root:has(', $selectorPostfix: ')') { + --graph-theme-opacity-factor: '0.5'; + + --color-graph-actionbar-background: color-mix(in srgb, #000 5%, var(--color-background)); + --color-graph-background: color-mix(in srgb, #000 5%, var(--color-background)); + --color-graph-background2: color-mix(in srgb, #000 10%, var(--color-background)); +} + +// Light DOM +body { + .vertical_scrollbar, + .horizontal_scrollbar { + border-color: transparent; + transition: border-color 1s linear; + } + + &:hover, + &:focus-within { + .vertical_scrollbar, + .horizontal_scrollbar { + transition: border-color 0.1s linear; + border-color: var(--vscode-scrollbarSlider-background); + } + } +} + +::-webkit-scrollbar { + width: var(--graph-column-scrollbar-thickness); + height: var(--graph-column-scrollbar-thickness); +} + +::-webkit-scrollbar-corner { + background-color: transparent !important; +} + +::-webkit-scrollbar-thumb { + background-color: transparent; + border-color: inherit; + border-right-style: inset; + border-right-width: calc(100vw + 100vh); + border-radius: unset !important; + + &:hover { + border-color: var(--vscode-scrollbarSlider-hoverBackground); + } + + &:active { + border-color: var(--vscode-scrollbarSlider-activeBackground); + } +} + +a { + text-decoration: none; + &:hover { + text-decoration: underline; + } +} + +a, +button:not([disabled]), +[tabindex]:not([tabindex='-1']) { + &:focus { + @include focusStyles(); + } +} + +// used in gitActionsButtons +// .pill { +// display: inline-block; +// padding: 0.2rem 0.5rem; +// border-radius: 0.5rem; +// font-size: 1rem; +// font-weight: 500; +// line-height: 1.2; +// text-transform: uppercase; +// color: var(--vscode-foreground); +// background-color: var(--vscode-editorWidget-background); + +// code-icon { +// font-size: inherit !important; +// line-height: inherit !important; +// } +// } + +// don't see usage +// .badge { +// font-size: 1rem; +// font-weight: 700; +// text-transform: uppercase; +// color: var(--color-foreground); + +// &.is-help { +// cursor: help; +// } + +// small { +// font-size: inherit; +// opacity: 0.6; +// font-weight: 400; +// } + +// &-container { +// position: relative; +// } + +// &-popover { +// width: max-content; +// right: 0; +// top: 100%; +// white-space: normal; +// } + +// &:not(:hover) + &-popover { +// display: none; +// } +// } + +// don't see usage +// .popover::part(body) { +// padding: 0; +// font-size: var(--vscode-font-size); +// background-color: var(--vscode-menu-background); +// } + +// header +// .jump-to-ref { +// --button-foreground: var(--color-foreground); +// } + +// header +// .shrink { +// max-width: fit-content; +// transition: all 0.2s; + +// &.hidden { +// max-width: 0; +// overflow: hidden; +// .titlebar__group &:not(:first-child) { +// // compensate the parent gap +// margin-left: -0.5rem; +// } +// } +// } + +// switch to actionButton in graph.css.ts +// .action-button { +// position: relative; +// appearance: none; +// font-family: inherit; +// font-size: 1.2rem; +// line-height: 2.2rem; +// // background-color: var(--color-graph-actionbar-background); +// background-color: transparent; +// border: none; +// color: inherit; +// color: var(--color-foreground); +// padding: 0 0.75rem; +// cursor: pointer; +// border-radius: 3px; +// height: auto; + +// display: grid; +// grid-auto-flow: column; +// grid-gap: 0.5rem; +// gap: 0.5rem; +// max-width: fit-content; + +// &[disabled] { +// pointer-events: none; +// cursor: default; +// opacity: 1; +// } + +// &:hover { +// background-color: var(--color-graph-actionbar-selectedBackground); +// color: var(--color-foreground); +// text-decoration: none; +// } + +// &[aria-checked] { +// border: 1px solid transparent; +// } + +// &[aria-checked='true'] { +// background-color: var(--vscode-inputOption-activeBackground); +// color: var(--vscode-inputOption-activeForeground); +// border-color: var(--vscode-inputOption-activeBorder); +// } + +// code-icon { +// line-height: 2.2rem; +// vertical-align: bottom; +// } +// code-icon[icon='graph-line'] { +// transform: translateY(1px); +// } + +// &__pill { +// .is-ahead & { +// background-color: var(--branch-status-ahead-pill-background); +// } +// .is-behind & { +// background-color: var(--branch-status-behind-pill-background); +// } +// .is-ahead.is-behind & { +// background-color: var(--branch-status-both-pill-background); +// } +// } + +// &__more, +// &__more.codicon[class*='codicon-'] { +// font-size: 1rem; +// margin-right: -0.25rem; +// } + +// code-icon#{&}__more::before { +// margin-left: -0.25rem; +// } + +// &__indicator { +// margin-left: -0.2rem; +// --gl-indicator-color: green; +// --gl-indicator-size: 0.4rem; +// } + +// &__small { +// font-size: smaller; +// opacity: 0.6; +// text-overflow: ellipsis; +// overflow: hidden; +// } + +// &__truncated { +// width: 100%; +// overflow: hidden; +// text-overflow: ellipsis; +// } + +// &.is-ahead { +// background-color: var(--branch-status-ahead-background); + +// &:hover { +// background-color: var(--branch-status-ahead-hover-background); +// } +// } +// &.is-behind { +// background-color: var(--branch-status-behind-background); + +// &:hover { +// background-color: var(--branch-status-behind-hover-background); +// } +// } +// &.is-ahead.is-behind { +// background-color: var(--branch-status-both-background); + +// &:hover { +// background-color: var(--branch-status-both-hover-background); +// } +// } +// } + +// header +// .action-divider { +// display: inline-block; +// width: 0.1rem; +// height: 2.2rem; +// vertical-align: middle; +// background-color: var(--titlebar-fg); +// opacity: 0.4; +// margin: { +// // left: 0.2rem; +// right: 0.2rem; +// } +// } + +// header +// .button-group { +// display: flex; +// flex-direction: row; +// align-items: stretch; + +// &:hover, +// &:focus-within { +// background-color: var(--color-graph-actionbar-selectedBackground); +// border-radius: 3px; +// } + +// > *:not(:first-child), +// > *:not(:first-child) .action-button { +// display: flex; +// border-top-left-radius: 0; +// border-bottom-left-radius: 0; +// } + +// > *:not(:first-child) .action-button { +// padding-left: 0.5rem; +// padding-right: 0.5rem; +// height: 100%; +// } + +// // > *:not(:last-child), +// // > *:not(:last-child) .action-button { +// // padding-right: 0.5rem; +// // } + +// &:hover > *:not(:last-child), +// &:active > *:not(:last-child), +// &:focus-within > *:not(:last-child), +// &:hover > *:not(:last-child) .action-button, +// &:active > *:not(:last-child) .action-button, +// &:focus-within > *:not(:last-child) .action-button { +// border-top-right-radius: 0; +// border-bottom-right-radius: 0; +// } + +// // > *:not(:first-child) { +// // border-left: 0.1rem solid var(--titlebar-fg); +// // } +// } + +// not used +// .repo-access { +// font-size: 1.1em; +// margin-right: 0.2rem; + +// &:not(.is-pro) { +// filter: grayscale(1) brightness(0.7); +// } +// } + +// used by graph component +.columns-settings { + --column-button-height: 19px; + + appearance: none; + font-family: inherit; + background-color: transparent; + border: none; + color: var(--color-graph-text-disabled, hsla(0, 0%, 100%, 0.4)); + height: var(--column-button-height); + cursor: pointer; + background-color: var(--color-graph-actionbar-background); + text-align: left; + border-radius: 3px; + + &:hover { + background-color: var(--vscode-toolbar-hoverBackground); + color: var(--color-foreground); + } + + &:focus { + @include focusStyles; + } + + &[disabled] { + pointer-events: none; + opacity: 0.5; + } + + code-icon { + font-size: 1.1rem; + position: relative; + } +} + +// Light DOM +.gk-graph { + width: 100%; + height: 100%; +} + +.gk-graph.bs-tooltip { + z-index: 1040; +} + +// header +// .flex-gap { +// display: flex; +// gap: 0.5em; +// align-items: center; +// } + +// not used +// .alert { +// --alert-foreground: var(--color-alert-foreground); +// --alert-background: var(--color-alert-infoBackground); +// --alert-border-color: var(--color-alert-infoBorder); +// --alert-hover-background: var(--color-alert-infoHoverBackground); +// display: flex; +// flex-direction: row; +// justify-content: flex-start; +// align-items: flex-start; +// gap: 1rem; +// padding: 1rem; +// border-radius: 0.25rem; +// border: 1px solid var(--alert-border-color); +// background-color: var(--alert-background); +// color: var(--alert-foreground); +// font-size: 1.2rem; +// // remove max-width and margin when converting to a web component or make it a variant/property +// max-width: 100rem; +// margin: { +// left: auto; +// right: auto; +// } + +// &__icon { +// &, +// &[class*='codicon-'] { +// font-size: 2rem; +// } +// } + +// &__content { +// flex: 1; +// padding-top: 0.1rem; + +// > *:not(:first-child) { +// margin-top: 0.75rem; +// } + +// > * { +// margin-bottom: 0; +// } + +// & a:not([class]) { +// color: currentColor; +// font-weight: 600; +// text-decoration: underline; +// } +// } + +// &__title { +// font-size: 1.3rem; +// font-weight: 600; +// text-transform: uppercase; +// margin-top: 0; +// } + +// &__accent { +// font-size: 1.1rem; + +// &-icon { +// margin-right: 0.2rem; +// line-height: 1.4rem; +// vertical-align: bottom; +// } +// } +// &__accent + &__accent { +// margin-top: 0.2rem; +// } + +// &__actions { +// display: flex; +// flex-direction: row; +// justify-content: flex-start; +// gap: 0.75rem; +// font-size: 1.1rem; +// } + +// &__dismiss { +// border: 1px solid transparent; +// background-color: transparent; +// color: inherit; +// appearance: none; +// width: 2rem; +// height: 2rem; +// padding: 0; +// } + +// &--warning { +// --alert-background: var(--color-alert-warningBackground); +// --alert-border-color: var(--color-alert-warningBorder); +// --alert-hover-background: var(--color-alert-warningHoverBackground); +// } + +// &--error { +// --alert-background: var(--color-alert-errorBackground); +// --alert-border-color: var(--color-alert-errorBorder); +// --alert-hover-background: var(--color-alert-errorHoverBackground); +// } + +// &--neutral { +// --alert-background: var(--color-alert-neutralBackground); +// --alert-border-color: var(--color-alert-neutralBorder); +// --alert-hover-background: var(--color-alert-neutralHoverBackground); +// } +// } + +// don't see usage +// .alert-action { +// display: inline-block; +// padding: 0.4rem 0.8rem; +// font-family: inherit; +// font-size: inherit; +// line-height: 1.4; +// text-align: center; +// text-decoration: none; +// user-select: none; +// background: transparent; +// color: var(--alert-foreground); +// cursor: pointer; +// border: 1px solid var(--alert-border-color); +// border-radius: 0.2rem; + +// &:hover { +// text-decoration: none; +// color: var(--alert-foreground); +// background-color: var(--alert-hover-background); +// } +// } + +// Light DOM +.graph-icon { + font: normal normal normal 14px/1 codicon; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + + vertical-align: middle; + line-height: 2rem; + letter-spacing: normal; + + &.mini-icon { + font-size: 1rem; + line-height: 1.6rem; + } +} + +// Light DOM +.icon { + &--head { + &::before { + font-family: codicon; + @include iconUtils.codicon('vm'); + } + } + &--remote { + &::before { + font-family: codicon; + @include iconUtils.codicon('cloud'); + } + } + &--remote-github, + &--remote-githubEnterprise { + &::before { + font-family: codicon; + @include iconUtils.codicon('github-inverted'); + } + } + &--remote-gitlab, + &--remote-gitlabSelfHosted { + &::before { + font-family: 'glicons'; + @include iconUtils.glicon('provider-gitlab'); + } + } + &--remote-bitbucket, + &--remote-bitbucketServer { + &::before { + font-family: 'glicons'; + @include iconUtils.glicon('provider-bitbucket'); + } + } + &--remote-azureDevops { + &::before { + font-family: 'glicons'; + @include iconUtils.glicon('provider-azdo'); + } + } + &--tag { + &::before { + font-family: codicon; + @include iconUtils.codicon('tag'); + } + } + &--stash { + &::before { + font-family: codicon; + @include iconUtils.codicon('inbox'); + } + } + &--check { + &::before { + font-family: codicon; + @include iconUtils.codicon('check'); + } + } + &--warning { + color: #de9b43; + :before { + font-family: codicon; + @include iconUtils.codicon('warning'); + } + } + &--added { + &::before { + font-family: codicon; + @include iconUtils.codicon('add'); + } + } + &--modified { + &::before { + font-family: codicon; + @include iconUtils.codicon('edit'); + } + } + &--deleted { + &::before { + font-family: codicon; + @include iconUtils.codicon('dash'); + } + } + &--renamed { + &::before { + font-family: codicon; + @include iconUtils.codicon('file'); + } + } + &--resolved { + &::before { + font-family: codicon; + @include iconUtils.codicon('pass-filled'); + } + } + &--hide { + &::before { + font-family: codicon; + @include iconUtils.codicon('eye-closed'); + } + } + &--show { + &::before { + font-family: codicon; + @include iconUtils.codicon('eye'); + } + } + &--pull-request { + &::before { + font-family: codicon; + @include iconUtils.codicon('git-pull-request'); + } + } + &--upstream-ahead { + &::before { + font-family: codicon; + @include iconUtils.codicon('arrow-up'); + } + } + &--upstream-behind { + &::before { + font-family: codicon; + @include iconUtils.codicon('arrow-down'); + } + } + &--settings { + &::before { + font-family: codicon; + @include iconUtils.codicon('settings-gear'); + } + } + &--branch { + &::before { + font-family: codicon; + @include iconUtils.codicon('git-branch'); + top: 0px; + margin: 0 0 0 0; + } + } + + &--graph { + &::before { + font-family: glicons; + @include iconUtils.glicon('graph'); + } + } + + &--commit { + &::before { + font-family: codicon; + @include iconUtils.codicon('git-commit'); + top: 0px; + margin: 0 0 0 0; + } + } + + &--author { + &::before { + font-family: codicon; + @include iconUtils.codicon('account'); + } + } + + &--datetime { + &::before { + font-family: glicons; + @include iconUtils.glicon('clock'); + } + } + + &--message { + &::before { + font-family: codicon; + @include iconUtils.codicon('comment'); + } + } + + &--changes { + &::before { + font-family: codicon; + @include iconUtils.codicon('request-changes'); + } + } + + &--files { + &::before { + font-family: codicon; + @include iconUtils.codicon('file'); + } + } + + &--worktree { + &::before { + font-family: glicons; + @include iconUtils.glicon('worktrees-view'); + } + } + + &--issue-github { + &::before { + font-family: codicon; + @include iconUtils.codicon('issues'); + } + } + + &--issue-gitlab { + &::before { + font-family: codicon; + @include iconUtils.codicon('issues'); + } + } + + &--issue-jiraCloud { + &::before { + font-family: 'glicons'; + @include iconUtils.glicon('provider-jira'); + } + } + + &--issue-azureDevops { + &::before { + font-family: codicon; + @include iconUtils.codicon('issues'); + } + } +} + +// header - move to graph.css.ts +// .titlebar { +// background: var(--titlebar-bg); +// color: var(--titlebar-fg); +// padding: { +// left: 0.8rem; +// right: 0.8rem; +// top: 0.6rem; +// bottom: 0.6rem; +// } +// font-size: 1.3rem; +// flex-wrap: wrap; + +// &, +// &__row, +// &__group { +// display: flex; +// flex-direction: row; +// align-items: center; +// gap: 0.5rem; + +// > * { +// margin: 0; +// } +// } + +// &, +// &__row { +// justify-content: space-between; +// } + +// &__row { +// flex: 0 0 100%; + +// &--wrap { +// display: grid; +// grid-auto-flow: column; +// justify-content: start; +// grid-template-columns: 1fr min-content; +// } +// } + +// &__group { +// flex: auto 1 1; +// } + +// &__row--wrap &__group { +// white-space: nowrap; + +// &:nth-child(odd) { +// min-width: 0; +// } +// } + +// &__debugging { +// > * { +// display: inline-block; +// } +// } + +// gl-feature-badge { +// color: var(--color-foreground); +// } +// } + +// gate +// gl-feature-gate gl-feature-badge { +// vertical-align: super; +// margin-left: 0.4rem; +// margin-right: 0.4rem; +// } + +// formerly .graph-app +body { + --fs-1: 1.1rem; + --fs-2: 1.3rem; + + padding: 0; + overflow: hidden; +} + +// might not need this +gl-graph-app, +gl-graph-app-wc, +// gl-graph-wrapper, +web-graph { + display: contents; +} + +// don't see usage +.graph-app { + // --fs-1: 1.1rem; + // --fs-2: 1.3rem; + + // padding: 0; + // overflow: hidden; + + // &__container { + // display: flex; + // flex-direction: column; + // height: calc(100vh - 2px); // shoot me -- the 2px is to stop the vertical scrollbar from showing up + // gap: 0; + // padding: 0.1rem; + // } + + // &__banners { + // flex: none; + // padding: 0.5rem; + // z-index: 2000; + + // &:empty { + // display: none; + // } + + // > *:not(:first-child) { + // margin-top: 0.5rem; + // } + // } + + &__gate { + // top: 40px; /* height of the header bar */ + padding-top: 40px; + } + + // header + // &__header { + // flex: none; + // z-index: 101; + // // width: fit-content; + // position: relative; + // } + + // &__footer { + // flex: none; + // } + + // &__main { + // flex: 1 1 auto; + // overflow: hidden; + // position: relative; + // display: flex; + // } + + // &__main.is-gated { + // position: relative; + // pointer-events: none; + // } +} + +// Light DOM +.gk-graph:not(.ref-zone):not([role='tooltip']) { + // flex: 1 1 auto; + position: relative; +} + +// Add when graph ref-zone "container" changes +// .gk-graph.ref-zone { +// position: absolute; +// } + +// Light DOM +.gk-graph .graph-header { + & .resizable-handle.horizontal { + --sash-size: 4px; + --sash-hover-size: 4px; + + border-right: none !important; + width: var(--sash-size) !important; + height: 100vh !important; + z-index: 1000; + + &:after { + content: ''; + border-left: 1px solid var(--titlebar-fg); + position: absolute; + top: 0.3rem; + left: 0.1rem; + height: 1.6rem; + width: var(--sash-size); + opacity: 0.3; + + transition: border-color 0.1s ease-out; + } + + &:before { + content: ''; + pointer-events: none; + position: absolute; + width: 100%; + height: 100vh; + transition: background-color 0.1s ease-out; + background: transparent; + + width: var(--sash-hover-size); + left: calc(50% - var(--sash-hover-size) / 2); + } + + &:hover, + &:active { + &:before { + transition-delay: 0.2s; + background-color: var(--vscode-sash-hoverBorder); + } + + &:after { + transition-delay: 0.2s; + border-left-color: var(--vscode-sash-hoverBorder); + } + } + + &:active:after { + content: ''; + position: absolute; + top: 0; + left: -100vw; + width: 200vw; + height: 100vh; + z-index: 1000; + } + } + + .columns-btn { + margin-top: 0.1rem; + } + + .button { + background-color: var(--color-graph-actionbar-background); + color: var(--color-graph-text-disabled, hsla(0deg, 0%, 100%, 0.4)); + border-radius: 3px; + + &:hover { + background-color: var(--vscode-toolbar-hoverBackground); + color: var(--color-foreground); + } + + &:focus { + @include focusStyles; + } + + &.active, + &.active:hover { + background-color: var(--vscode-toolbar-activeBackground); + color: var(--color-foreground); + } + } + + .graph-icon { + color: var(--color-graph-text-disabled, hsla(0, 0%, 100%, 0.4)); + } +} + +// Light DOM +.graph-app:not(:hover) { + .gk-graph .graph-header { + & .resizable-handle.horizontal:before { + display: none; + } + } +} + +// Light DOM +.graph-container { + & .resizable-handle.horizontal { + display: none; + } + + & .node.stash-node .graph-icon { + transform: translateY(-2px); + } + + & .graph-adjust-commit-count { + display: flex; + width: calc(100vw - var(--graph-column-scrollbar-thickness)); + align-items: center; + justify-content: center; + } + + & .changes-zone.changes-bar .changes-sub-bar.deleted { + margin-left: 2px; + } +} + +// don't see usage +.mr-loose { + margin-right: 0.5rem; +} + +// gl-graph-header +// .progress-container { +// position: absolute; +// left: 0; +// bottom: 0; +// z-index: 5; +// height: 2px; +// width: 100%; +// overflow: hidden; + +// & .progress-bar { +// background-color: var(--vscode-progressBar-background); +// display: none; +// position: absolute; +// left: 0; +// width: 2%; +// height: 2px; +// } + +// &.active .progress-bar { +// display: inherit; +// } + +// &.discrete .progress-bar { +// left: 0; +// transition: width 0.1s linear; +// } + +// &.discrete.done .progress-bar { +// width: 100%; +// } + +// &.infinite .progress-bar { +// animation-name: progress; +// animation-duration: 4s; +// animation-iteration-count: infinite; +// animation-timing-function: steps(100); +// transform: translateZ(0); +// } +// } + +// @keyframes progress { +// 0% { +// transform: translateX(0) scaleX(1); +// } + +// 50% { +// transform: translateX(2500%) scaleX(3); +// } + +// to { +// transform: translateX(4900%) scaleX(1); +// } +// } + +// don't see usage +// .sr-only { +// clip: rect(0 0 0 0); +// clip-path: inset(50%); +// height: 1px; +// overflow: hidden; +// position: absolute; +// white-space: nowrap; +// width: 1px; +// } + +// don't see usage +// #opts-popover { +// font-size: var(--vscode-font-size); +// font-family: var(--vscode-font-family); +// background-color: var(--vscode-menu-background); +// border: 1px solid var(--vscode-menu-border); +// padding: 0; +// z-index: 1001; + +// ul > li { +// padding: 0 0.6rem; +// height: 2.2rem; +// line-height: 2.2rem; +// color: var(--vscode-menu-foreground); +// background-color: var(--vscode-menu-background); + +// &:hover { +// color: var(--vscode-menu-selectionForeground); +// background-color: var(--vscode-menu-selectionBackground); +// } +// } +// } + +// Light DOM +.gk-graph .tooltip, +.gk-graph.tooltip { + font-size: var(--vscode-font-size) !important; + font-family: var(--vscode-font-family) !important; + font-weight: normal !important; + line-height: 19px !important; + + &.show { + opacity: 1; + } + + &.tooltip-arrow:after { + background-color: var(--color-hover-background) !important; + border-right-color: var(--color-hover-border) !important; + border-bottom-color: var(--color-hover-border) !important; + } + + &-inner { + font-size: var(--vscode-font-size) !important; + padding: 4px 10px 5px 10px !important; + color: var(--color-hover-foreground) !important; + background-color: var(--color-hover-background) !important; + border: 1px solid var(--color-hover-border) !important; + border-radius: 0 !important; + box-shadow: 0 2px 8px var(--vscode-widget-shadow) !important; + text-align: start !important; + white-space: break-spaces !important; + } +} + +// gl-graph-header +// .minimap-marker-swatch { +// display: inline-block; +// width: 1rem; +// height: 1rem; +// border-radius: 2px; +// transform: scale(1.6); +// margin-left: 0.3rem; +// margin-right: 1rem; + +// &[data-marker='localBranches'] { +// background-color: var(--color-graph-minimap-marker-local-branches); +// } + +// &[data-marker='pullRequests'] { +// background-color: var(--color-graph-minimap-marker-pull-requests); +// } + +// &[data-marker='remoteBranches'] { +// background-color: var(--color-graph-minimap-marker-remote-branches); +// } + +// &[data-marker='stashes'] { +// background-color: var(--color-graph-minimap-marker-stashes); +// } + +// &[data-marker='tags'] { +// background-color: var(--color-graph-minimap-marker-tags); +// } +// } + +// gl-graph-header - switch to ruleBase in graph.css.ts +// hr { +// border: none; +// border-top: 1px solid var(--color-foreground--25); +// } + +// gl-graph-header - switch to .inline-code +// .md-code { +// background: var(--vscode-textCodeBlock-background); +// border-radius: 3px; +// padding: 0px 4px 2px 4px; +// font-family: var(--vscode-editor-font-family); +// } + +// gl-graph-header +// gl-search-box::part(search) { +// --gl-search-input-background: var(--color-graph-actionbar-background); +// --gl-search-input-border: var(--sl-input-border-color); +// } + +// gl-graph-header +// sl-option::part(base) { +// padding: 0.2rem 0.4rem; +// } + +// sl-option[aria-selected='true']::part(base), +// sl-option:not([aria-selected='true']):hover::part(base), +// sl-option:not([aria-selected='true']):focus::part(base) { +// background-color: var(--vscode-list-activeSelectionBackground); +// color: var(--vscode-list-activeSelectionForeground); +// } + +// sl-option::part(checked-icon) { +// display: none; +// } + +// gl-graph-header +// sl-select::part(listbox) { +// padding-block: 0.2rem 0; +// width: max-content; +// } + +// sl-select::part(combobox) { +// --sl-input-background-color: var(--color-graph-actionbar-background); +// --sl-input-color: var(--color-foreground); +// --sl-input-color-hover: var(--color-foreground); +// padding: 0 0.75rem; +// color: var(--color-foreground); +// border-radius: var(--sl-border-radius-small); +// } + +// sl-select::part(display-input) { +// field-sizing: content; +// } + +// sl-select::part(expand-icon) { +// margin-inline-start: var(--sl-spacing-x-small); +// } + +// sl-select[open]::part(combobox) { +// background-color: var(--color-graph-actionbar-background); +// } +// sl-select:hover::part(combobox), +// sl-select:focus::part(combobox) { +// background-color: var(--color-graph-actionbar-selectedBackground); +// } + +// gl-graph-header +// .merge-conflict-warning { +// flex: 0 0 100%; +// min-width: 0; +// } + +// light DOM +.graph { + box-sizing: border-box; + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; + padding: 0.1rem; + + &__header { + flex: none; + z-index: 101; + position: relative; + } + + &__workspace { + position: relative; + flex: 1; + overflow: hidden; + } + + &__panes { + } + + &__graph-pane { + display: grid; + grid-template-columns: min-content 1fr; + grid-template-rows: 1fr; + grid-template-areas: 'minimap minimap' 'sidebar graph'; + height: 100%; + overflow: hidden; + + gl-graph-minimap-container { + grid-area: minimap; + } + + gl-graph-sidebar { + grid-area: sidebar; + } + + gl-graph-wrapper { + grid-area: graph; + } + } +} diff --git a/src/webviews/apps/plus/graph-next/graph.ts b/src/webviews/apps/plus/graph-next/graph.ts new file mode 100644 index 0000000000000..9de026e2e8ce3 --- /dev/null +++ b/src/webviews/apps/plus/graph-next/graph.ts @@ -0,0 +1,272 @@ +/*global document window*/ +import type { CssVariables } from '@gitkraken/gitkraken-components'; +import { provide } from '@lit/context'; +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 type { HostIpc } from '../../shared/ipc'; +import type { ThemeChangeEvent } from '../../shared/theme'; +import type { GraphAppWC } from './graph-app'; +import { GraphAppState, graphStateContext, GraphStateProvider } from './stateProvider'; +import './graph-app'; +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'], +]); + +@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``; + } +} diff --git a/src/webviews/apps/plus/graph-next/sidebar/sidebar.ts b/src/webviews/apps/plus/graph-next/sidebar/sidebar.ts new file mode 100644 index 0000000000000..0b07cdaabb587 --- /dev/null +++ b/src/webviews/apps/plus/graph-next/sidebar/sidebar.ts @@ -0,0 +1,182 @@ +import { consume } from '@lit/context'; +import { Task } from '@lit/task'; +import { css, html, LitElement, nothing } from 'lit'; +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 { ipcContext } from '../../../shared/contexts/ipc'; +import type { Disposable } from '../../../shared/events'; +import type { HostIpc } from '../../../shared/ipc'; +import { emitTelemetrySentEvent } from '../../../shared/telemetry'; +import { stateContext } from '../context'; +import '../../../shared/components/code-icon'; +import '../../../shared/components/overlays/tooltip'; + +interface Icon { + type: IconTypes; + icon: string; + command: string; + tooltip: string; +} +type IconTypes = 'branches' | 'remotes' | 'stashes' | 'tags' | 'worktrees'; +const icons: Icon[] = [ + { type: 'branches', icon: 'gl-branches-view', command: 'gitlens.showBranchesView', tooltip: 'Branches' }, + { type: 'remotes', icon: 'gl-remotes-view', command: 'gitlens.showRemotesView', tooltip: 'Remotes' }, + { type: 'stashes', icon: 'gl-stashes-view', command: 'gitlens.showStashesView', tooltip: 'Stashes' }, + { type: 'tags', icon: 'gl-tags-view', command: 'gitlens.showTagsView', tooltip: 'Tags' }, + { type: 'worktrees', icon: 'gl-worktrees-view', command: 'gitlens.showWorktreesView', tooltip: 'Worktrees' }, +]; + +type Counts = Record; + +@customElement('gl-graph-sidebar') +export class GlGraphSideBar extends LitElement { + static override styles = css` + .sidebar { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.4rem; + background-color: var(--color-graph-background); + color: var(--titlebar-fg); + width: 2.6rem; + font-size: 9px; + font-weight: 600; + height: 100vh; + padding: 3rem 0; + z-index: 1040; + } + + .item { + color: inherit; + text-decoration: none; + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + } + + .item:hover { + color: var(--color-foreground); + text-decoration: none; + } + + .count { + color: var(--color-foreground--50); + /* color: var(--color-highlight); */ + margin-top: 0.4rem; + } + + .count.error { + color: var(--vscode-errorForeground); + opacity: 0.6; + } + `; + + 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; + + @consume({ context: stateContext, subscribe: true }) + private readonly _state!: State; + + private _disposable: Disposable | undefined; + private _countsTask = new Task(this, { + args: () => [this.fetchCounts()], + task: ([counts]) => counts, + autoRun: false, + }); + + override connectedCallback(): void { + super.connectedCallback(); + + this._disposable = this._ipc.onReceiveMessage(msg => { + switch (true) { + case DidChangeNotification.is(msg): + this._counts = undefined; + this.requestUpdate(); + break; + + case GetCountsRequest.response.is(msg): + this._counts = Promise.resolve(msg.params as Counts); + this.requestUpdate(); + break; + } + }); + } + + override disconnectedCallback(): void { + super.disconnectedCallback(); + + this._disposable?.dispose(); + } + + private _counts: Promise | undefined; + private async fetchCounts() { + if (this._counts == null) { + const ipc = this._ipc; + if (ipc != null) { + async function fetch() { + const rsp = await ipc.sendRequest(GetCountsRequest, undefined); + return rsp as Counts; + } + this._counts = fetch(); + } else { + this._counts = Promise.resolve(undefined); + } + } + return this._counts; + } + + override render(): unknown { + if (this._counts == null) { + void this._countsTask.run(); + } + + return html``; + } + + private renderIcon(icon: Icon) { + if (this.include != null && !this.include.includes(icon.type)) return; + + return html` + this.sendTelemetry(icon.command)}> + + ${this._countsTask.render({ + pending: () => + html``, + complete: c => renderCount(c?.[icon.type]), + error: () => html``, + })} + + `; + } + + private sendTelemetry(command: string) { + emitTelemetrySentEvent<'graph/action/sidebar'>(this, { + name: 'graph/action/sidebar', + data: { action: command }, + }); + } +} + +function renderCount(count: number | undefined) { + if (count == null) return nothing; + + return html`${count > 999 ? '1K+' : String(count)}`; +} diff --git a/src/webviews/apps/plus/graph-next/stateProvider.ts b/src/webviews/apps/plus/graph-next/stateProvider.ts new file mode 100644 index 0000000000000..0897b572bff3b --- /dev/null +++ b/src/webviews/apps/plus/graph-next/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/contexts/logger'; +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/graph-next/styles/graph.css.ts b/src/webviews/apps/plus/graph-next/styles/graph.css.ts new file mode 100644 index 0000000000000..a18f69cfb2d7b --- /dev/null +++ b/src/webviews/apps/plus/graph-next/styles/graph.css.ts @@ -0,0 +1,138 @@ +import { css } from 'lit'; +import { focusOutline } from '../../../shared/components/styles/lit/a11y.css'; + +export const linkBase = css` + a { + text-decoration: none; + } + + a:focus { + ${focusOutline} + } + + a:hover { + text-decoration: underline; + } +`; + +export const ruleBase = css` + hr { + border: none; + border-top: 1px solid var(--color-foreground--25); + } +`; + +export const actionButton = css` + .action-button { + position: relative; + appearance: none; + font-family: inherit; + font-size: 1.2rem; + line-height: 2.2rem; + // background-color: var(--color-graph-actionbar-background); + background-color: transparent; + border: none; + color: inherit; + color: var(--color-foreground); + padding: 0 0.75rem; + cursor: pointer; + border-radius: 3px; + height: auto; + + display: grid; + grid-auto-flow: column; + grid-gap: 0.5rem; + gap: 0.5rem; + max-width: fit-content; + } + + .action-button[disabled] { + pointer-events: none; + cursor: default; + opacity: 1; + } + + .action-button:hover { + background-color: var(--color-graph-actionbar-selectedBackground); + color: var(--color-foreground); + text-decoration: none; + } + + .action-button[aria-checked] { + border: 1px solid transparent; + } + + .action-button[aria-checked='true'] { + background-color: var(--vscode-inputOption-activeBackground); + color: var(--vscode-inputOption-activeForeground); + border-color: var(--vscode-inputOption-activeBorder); + } + + .action-button code-icon { + line-height: 2.2rem; + vertical-align: bottom; + } + .action-button code-icon[icon='graph-line'] { + transform: translateY(1px); + } + + .is-ahead .action-button__pill { + background-color: var(--branch-status-ahead-pill-background); + } + .is-behind .action-button__pill { + background-color: var(--branch-status-behind-pill-background); + } + .is-ahead.is-behind .action-button__pill { + background-color: var(--branch-status-both-pill-background); + } + + .action-button__more, + .action-button__more.codicon[class*='codicon-'] { + font-size: 1rem; + margin-right: -0.25rem; + } + + code-icon.action-button__more::before { + margin-left: -0.25rem; + } + + .action-button__indicator { + margin-left: -0.2rem; + --gl-indicator-color: green; + --gl-indicator-size: 0.4rem; + } + + .action-button__small { + font-size: smaller; + opacity: 0.6; + text-overflow: ellipsis; + overflow: hidden; + } + + .action-button__truncated { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } + + .action-button.is-ahead { + background-color: var(--branch-status-ahead-background); + } + .action-button.is-ahead:hover { + background-color: var(--branch-status-ahead-hover-background); + } + + .action-button.is-behind { + background-color: var(--branch-status-behind-background); + } + .action-button.is-behind:hover { + background-color: var(--branch-status-behind-hover-background); + } + + .action-button.is-ahead.is-behind { + background-color: var(--branch-status-both-background); + } + .action-button.is-ahead.is-behind:hover { + background-color: var(--branch-status-both-hover-background); + } +`; diff --git a/src/webviews/apps/plus/graph-next/styles/header.css.ts b/src/webviews/apps/plus/graph-next/styles/header.css.ts new file mode 100644 index 0000000000000..df80b4e025925 --- /dev/null +++ b/src/webviews/apps/plus/graph-next/styles/header.css.ts @@ -0,0 +1,272 @@ +import { css } from 'lit'; + +export const repoHeaderStyles = css` + .jump-to-ref { + --button-foreground: var(--color-foreground); + } + + .merge-conflict-warning { + flex: 0 0 100%; + min-width: 0; + } +`; + +export const progressStyles = css` + .progress-container { + position: absolute; + left: 0; + bottom: 0; + z-index: 5; + height: 2px; + width: 100%; + overflow: hidden; + } + .progress-container .progress-bar { + background-color: var(--vscode-progressBar-background); + display: none; + position: absolute; + left: 0; + width: 2%; + height: 2px; + } + + .progress-container.active .progress-bar { + display: inherit; + } + + .progress-container.discrete .progress-bar { + left: 0; + transition: width 0.1s linear; + } + + .progress-container.discrete.done .progress-bar { + width: 100%; + } + + .progress-container.infinite .progress-bar { + animation-name: progress; + animation-duration: 4s; + animation-iteration-count: infinite; + animation-timing-function: steps(100); + transform: translateZ(0); + } + + @keyframes progress { + 0% { + transform: translateX(0) scaleX(1); + } + + 50% { + transform: translateX(2500%) scaleX(3); + } + + to { + transform: translateX(4900%) scaleX(1); + } + } +`; + +export const titlebarStyles = css` + .titlebar { + background: var(--titlebar-bg); + color: var(--titlebar-fg); + padding: 0.6rem 0.8rem; + font-size: 1.3rem; + flex-wrap: wrap; + } + .titlebar, + .titlebar__row, + .titlebar__group { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + } + + .titlebar > *, + .titlebar__row > *, + .titlebar__group > * { + margin: 0; + } + + .titlebar, + .titlebar__row { + justify-content: space-between; + } + + .titlebar__row { + flex: 0 0 100%; + } + .titlebar__row--wrap { + display: grid; + grid-auto-flow: column; + justify-content: start; + grid-template-columns: 1fr min-content; + } + + .titlebar__group { + flex: auto 1 1; + } + + .titlebar__row--wrap .titlebar__group { + white-space: nowrap; + } + .titlebar__row--wrap .titlebar__group:nth-child(odd) { + min-width: 0; + } + + .titlebar__debugging > * { + display: inline-block; + } + + .titlebar gl-feature-badge { + color: var(--color-foreground); + } +`; + +export const graphHeaderControlStyles = css` + .shrink { + max-width: fit-content; + transition: all 0.2s; + } + .shrink.hidden { + max-width: 0; + overflow: hidden; + } + .titlebar__group .shrink.hidden:not(:first-child) { + // compensate the parent gap + margin-left: -0.5rem; + } + + .flex-gap { + display: flex; + gap: 0.5em; + align-items: center; + } + + .action-divider { + display: inline-block; + width: 0.1rem; + height: 2.2rem; + vertical-align: middle; + background-color: var(--titlebar-fg); + opacity: 0.4; + margin: { + // left: 0.2rem; + right: 0.2rem; + } + } + + .button-group { + display: flex; + flex-direction: row; + align-items: stretch; + } + .button-group:hover, + .button-group:focus-within { + background-color: var(--color-graph-actionbar-selectedBackground); + border-radius: 3px; + } + + .button-group > *:not(:first-child), + .button-group > *:not(:first-child) .action-button { + display: flex; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .button-group > *:not(:first-child) .action-button { + padding-left: 0.5rem; + padding-right: 0.5rem; + height: 100%; + } + + .button-group:hover > *:not(:last-child), + .button-group:active > *:not(:last-child), + .button-group:focus-within > *:not(:last-child), + .button-group:hover > *:not(:last-child) .action-button, + .button-group:active > *:not(:last-child) .action-button, + .button-group:focus-within > *:not(:last-child) .action-button { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .minimap-marker-swatch { + display: inline-block; + width: 1rem; + height: 1rem; + border-radius: 2px; + transform: scale(1.6); + margin-left: 0.3rem; + margin-right: 1rem; + } + + .minimap-marker-swatch[data-marker='localBranches'] { + background-color: var(--color-graph-minimap-marker-local-branches); + } + + .minimap-marker-swatch[data-marker='pullRequests'] { + background-color: var(--color-graph-minimap-marker-pull-requests); + } + + .minimap-marker-swatch[data-marker='remoteBranches'] { + background-color: var(--color-graph-minimap-marker-remote-branches); + } + + .minimap-marker-swatch[data-marker='stashes'] { + background-color: var(--color-graph-minimap-marker-stashes); + } + + .minimap-marker-swatch[data-marker='tags'] { + background-color: var(--color-graph-minimap-marker-tags); + } + + gl-search-box::part(search) { + --gl-search-input-background: var(--color-graph-actionbar-background); + --gl-search-input-border: var(--sl-input-border-color); + } + + sl-option::part(base) { + padding: 0.2rem 0.4rem; + } + + sl-option[aria-selected='true']::part(base), + sl-option:not([aria-selected='true']):hover::part(base), + sl-option:not([aria-selected='true']):focus::part(base) { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); + } + + sl-option::part(checked-icon) { + display: none; + } + + sl-select::part(listbox) { + padding-block: 0.2rem 0; + width: max-content; + } + + sl-select::part(combobox) { + --sl-input-background-color: var(--color-graph-actionbar-background); + --sl-input-color: var(--color-foreground); + --sl-input-color-hover: var(--color-foreground); + padding: 0 0.75rem; + color: var(--color-foreground); + border-radius: var(--sl-border-radius-small); + } + + sl-select::part(display-input) { + field-sizing: content; + } + + sl-select::part(expand-icon) { + margin-inline-start: var(--sl-spacing-x-small); + } + + sl-select[open]::part(combobox) { + background-color: var(--color-graph-actionbar-background); + } + sl-select:hover::part(combobox), + sl-select:focus::part(combobox) { + background-color: var(--color-graph-actionbar-selectedBackground); + } +`; diff --git a/src/webviews/apps/plus/graph-next/tsconfig.json b/src/webviews/apps/plus/graph-next/tsconfig.json new file mode 100644 index 0000000000000..e821403b88d2d --- /dev/null +++ b/src/webviews/apps/plus/graph-next/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "paths": { + "@env/*": ["src/env/browser/*"], + "@gitkraken/gitkraken-components": ["../../../../node_modules/@gitkraken/gitkraken-components-next"], + "@types/react": ["../../../../node_modules/@types/react-next"], + "@types/react-dom": ["../../../../node_modules/@types/react-dom-next"], + "react": ["../../../../node_modules/react-next"], + "react-dom": ["../../../../node_modules/react-dom-next"] + } + } +} diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 0d9b9ae5818bc..0a03b7d83b378 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -73,7 +73,7 @@ import { import type { IpcNotification } from '../../../protocol'; import { DidChangeHostWindowFocusNotification } from '../../../protocol'; import { GlButton } from '../../shared/components/button.react'; -import { GlCheckbox } from '../../shared/components/checkbox'; +import { GlCheckbox } from '../../shared/components/checkbox/checkbox.react'; import { CodeIcon } from '../../shared/components/code-icon.react'; import { GlIndicator } from '../../shared/components/indicators/indicator.react'; import { GlMarkdown } from '../../shared/components/markdown/markdown.react'; diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index 7bc2abb4d2b3a..e0018b6ea7881 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -134,7 +134,9 @@ export class GraphApp extends App { $root, ); disposables.push({ - dispose: () => unmountComponentAtNode($root), + dispose: () => { + unmountComponentAtNode($root); + }, }); } diff --git a/src/webviews/apps/plus/graph/hover/graphHover.ts b/src/webviews/apps/plus/graph/hover/graphHover.ts index 2a5831f39341f..ab449438c975c 100644 --- a/src/webviews/apps/plus/graph/hover/graphHover.ts +++ b/src/webviews/apps/plus/graph/hover/graphHover.ts @@ -7,7 +7,7 @@ import { debounce } from '../../../../../system/function/debounce'; 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 type { GlPopover } from '../../../shared/components/overlays/popover'; import '../../../shared/components/markdown/markdown'; import '../../../shared/components/overlays/popover'; 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.ts b/src/webviews/apps/plus/graph/sidebar/sidebar.ts index e4c5d8b868f59..d6ef1d02af4a4 100644 --- a/src/webviews/apps/plus/graph/sidebar/sidebar.ts +++ b/src/webviews/apps/plus/graph/sidebar/sidebar.ts @@ -7,9 +7,9 @@ import { DidChangeNotification, GetCountsRequest } from '../../../../plus/graph/ import { ipcContext } from '../../../shared/contexts/ipc'; import type { Disposable } from '../../../shared/events'; import type { HostIpc } from '../../../shared/ipc'; +import { emitTelemetrySentEvent } from '../../../shared/telemetry'; import '../../../shared/components/code-icon'; import '../../../shared/components/overlays/tooltip'; -import { emitTelemetrySentEvent } from '../../../shared/telemetry'; interface Icon { type: IconTypes; diff --git a/src/webviews/apps/shared/app.ts b/src/webviews/apps/shared/app.ts index 2c43bab90ad10..12b392df52c54 100644 --- a/src/webviews/apps/shared/app.ts +++ b/src/webviews/apps/shared/app.ts @@ -14,6 +14,8 @@ import { promosContext, PromosContext } from './contexts/promos'; import { telemetryContext, TelemetryContext } from './contexts/telemetry'; import type { Disposable } from './events'; import { HostIpc } from './ipc'; +import type { ThemeChangeEvent } from './theme'; +import { computeThemeColors, onDidChangeTheme, watchThemeColors } from './theme'; export type ReactiveElementHost = ReactiveControllerHost & HTMLElement; @@ -50,6 +52,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; @@ -77,6 +80,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.ts b/src/webviews/apps/shared/components/button.ts index ba6d310bfe0c6..9b6c8c7b43b60 100644 --- a/src/webviews/apps/shared/components/button.ts +++ b/src/webviews/apps/shared/components/button.ts @@ -211,26 +211,38 @@ export class GlButton extends LitElement { @property() href?: string; - @property({ reflect: true }) - // eslint-disable-next-line lit/no-native-attributes - override get role(): 'link' | 'button' { - return this.href ? 'link' : 'button'; - } - @property() tooltip?: string; @property() tooltipPlacement?: GlTooltip['placement'] = 'bottom'; - protected override updated(changedProperties: PropertyValueMap | Map): void { - super.updated(changedProperties); + override connectedCallback(): void { + super.connectedCallback(); - if (changedProperties.has('disabled')) { + this.setAttribute('role', this.href ? 'link' : 'button'); + if (this.disabled) { this.setAttribute('aria-disabled', this.disabled.toString()); } } + protected override willUpdate(changedProperties: PropertyValueMap | Map): void { + if (changedProperties.has('href')) { + this.setAttribute('role', this.href ? 'link' : 'button'); + } + + if (changedProperties.has('disabled')) { + const disabled = changedProperties.get('disabled'); + if (disabled) { + this.setAttribute('aria-disabled', disabled.toString()); + } else { + this.removeAttribute('aria-disabled'); + } + } + + super.willUpdate(changedProperties); + } + protected override render(): unknown { if (this.tooltip) { return html` { 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/apps/shared/components/styles/lit/base.css.ts b/src/webviews/apps/shared/components/styles/lit/base.css.ts index f7a855663ff42..0aaba47ebea31 100644 --- a/src/webviews/apps/shared/components/styles/lit/base.css.ts +++ b/src/webviews/apps/shared/components/styles/lit/base.css.ts @@ -66,3 +66,12 @@ export const scrollableBase = css` transition: none; } `; + +export const inlineCode = css` + .inline-code { + background: var(--vscode-textCodeBlock-background); + border-radius: 3px; + padding: 0px 4px 2px 4px; + font-family: var(--vscode-editor-font-family); + } +`; diff --git a/src/webviews/plus/graph/graphWebview.ts b/src/webviews/plus/graph/graphWebview.ts index 96ba2ac0d7499..181e9cb02d55c 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'; @@ -89,11 +88,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'; @@ -121,13 +119,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, @@ -138,31 +144,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, @@ -170,10 +165,7 @@ import type { GraphScrollMarkerTypes, GraphSearchResults, GraphSelectedRows, - GraphStashContextValue, - GraphTagContextValue, GraphUpstreamMetadata, - GraphUpstreamStatusContextValue, GraphWorkingTreeStats, OpenPullRequestDetailsParams, SearchOpenInViewParams, @@ -1291,7 +1283,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'; - case HostingIntegrationId.AzureDevOps: - case 'azure': - case 'azure-devops': - // TODO: Remove the casting once this is officially recognized by the component - return 'azureDevops' as GraphIssueTrackerType; - case 'bitbucket': - // TODO: Remove the casting once this is officially recognized by the component - return HostingIntegrationId.Bitbucket as GraphIssueTrackerType; - 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..3db85f3b53d16 --- /dev/null +++ b/src/webviews/plus/graph/graphWebviewUtils.ts @@ -0,0 +1,161 @@ +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'; + case HostingIntegrationId.AzureDevOps: + case 'azure': + case 'azure-devops': + // TODO: Remove the casting once this is officially recognized by the component + return 'azureDevops' as GraphIssueTrackerType; + case 'bitbucket': + // TODO: Remove the casting once this is officially recognized by the component + return HostingIntegrationId.Bitbucket as GraphIssueTrackerType; + default: + return undefined; + } +} diff --git a/src/webviews/plus/graph/registration.ts b/src/webviews/plus/graph/registration.ts index 34592e1f1c2ca..80a3b6bb6484e 100644 --- a/src/webviews/plus/graph/registration.ts +++ b/src/webviews/plus/graph/registration.ts @@ -24,6 +24,10 @@ import type { ShowInCommitGraphCommandArgs, State } from './protocol'; export type GraphWebviewShowingArgs = [Repository | { ref: GitReference }]; +function getFileName(): string { + return configuration.get('graph.experimental.renderer.enabled') ? 'graph-next.html' : 'graph.html'; +} + export function registerGraphWebviewPanel( controller: WebviewsController, ): WebviewPanelsProxy<'gitlens.graph', GraphWebviewShowingArgs, State> { @@ -31,7 +35,7 @@ export function registerGraphWebviewPanel( { id: GlCommand.ShowGraphPage, options: { preserveInstance: true } }, { id: 'gitlens.graph', - fileName: 'graph.html', + fileName: getFileName(), iconPath: 'images/gitlens-icon.png', title: 'Commit Graph', contextKeyPrefix: `gitlens:webview:graph`, @@ -58,7 +62,7 @@ export function registerGraphWebviewView( return controller.registerWebviewView<'gitlens.views.graph', State, State, GraphWebviewShowingArgs>( { id: 'gitlens.views.graph', - fileName: 'graph.html', + fileName: getFileName(), title: 'Commit Graph', contextKeyPrefix: `gitlens:webviewView:graph`, trackingFeature: 'graphView', diff --git a/webpack.config.mjs b/webpack.config.mjs index bb87d17c06e2c..334888982660d 100644 --- a/webpack.config.mjs +++ b/webpack.config.mjs @@ -322,6 +322,24 @@ function getWebviewsConfigs(mode, env) { mode, env, ), + getWebviewConfig( + { + 'graph-next': { entry: './plus/graph-next/graph.ts', plus: true }, + }, + { + alias: { + '@gitkraken/gitkraken-components': path.resolve( + __dirname, + 'node_modules', + '@gitkraken/gitkraken-components-next', + ), + react: path.resolve(__dirname, 'node_modules', 'react-next'), + 'react-dom': path.resolve(__dirname, 'node_modules', 'react-dom-next'), + }, + }, + mode, + env, + ), ]; }