diff --git a/src/webviews/apps/commitDetails/commitDetails.html b/src/webviews/apps/commitDetails/commitDetails.html index a69627b889131..e7a8c40a2ed13 100644 --- a/src/webviews/apps/commitDetails/commitDetails.html +++ b/src/webviews/apps/commitDetails/commitDetails.html @@ -14,6 +14,9 @@ src: url('#{root}/dist/glicons.woff2?d3b316716ee1329763a193a20834cd0a') format('woff2'); } + - - #{endOfBody} + diff --git a/src/webviews/apps/commitDetails/commitDetails.ts b/src/webviews/apps/commitDetails/commitDetails.ts index c7f468a012420..68631efdf7969 100644 --- a/src/webviews/apps/commitDetails/commitDetails.ts +++ b/src/webviews/apps/commitDetails/commitDetails.ts @@ -1,26 +1,605 @@ -/*global*/ +import { html, nothing } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { when } from 'lit/directives/when.js'; +import type { ViewFilesLayout } from '../../../config'; +import type { GlCommands } from '../../../constants.commands'; import type { Serialized } from '../../../system/serialize'; -import type { State } from '../../commitDetails/protocol'; -import { App } from '../shared/appBase'; +import { pluralize } from '../../../system/string'; +import type { DraftState, ExecuteCommitActionsParams, Mode, State } from '../../commitDetails/protocol'; +import { + ChangeReviewModeCommand, + CreatePatchFromWipCommand, + ExecuteCommitActionCommand, + ExecuteFileActionCommand, + ExplainRequest, + FetchCommand, + GenerateRequest, + NavigateCommand, + OpenFileCommand, + OpenFileComparePreviousCommand, + OpenFileCompareWorkingCommand, + OpenFileOnRemoteCommand, + OpenPullRequestChangesCommand, + OpenPullRequestComparisonCommand, + OpenPullRequestDetailsCommand, + OpenPullRequestOnRemoteCommand, + PickCommitCommand, + PinCommand, + PublishCommand, + PullCommand, + PushCommand, + SearchCommitCommand, + ShowCodeSuggestionCommand, + StageFileCommand, + SuggestChangesCommand, + SwitchCommand, + UnstageFileCommand, +} from '../../commitDetails/protocol'; +import { ExecuteCommand } from '../../protocol'; +import type { CreatePatchMetadataEventDetail } from '../plus/patchDetails/components/gl-patch-create'; +import { GlAppHost } from '../shared/appHost'; +import type { IssuePullRequest } from '../shared/components/rich/issue-pull-request'; +import type { WebviewPane, WebviewPaneExpandedChangeEventDetail } from '../shared/components/webview-pane'; import { DOM } from '../shared/dom'; -import type { GlCommitDetailsApp } from './components/commit-details-app'; +import type { HostIpc } from '../shared/ipc'; +import type { GlCommitDetails } from './components/gl-commit-details'; +import type { FileChangeListItemDetail } from './components/gl-details-base'; +import type { GlInspectNav } from './components/gl-inspect-nav'; +import type { CreatePatchEventDetail, GenerateState } from './components/gl-inspect-patch'; +import type { GlWipDetails } from './components/gl-wip-details'; +import { CommitDetailsStateProvider } from './stateProvider'; import './commitDetails.scss'; -import './components/commit-details-app'; +import '../shared/components/code-icon'; +import '../shared/components/indicators/indicator'; +import '../shared/components/overlays/tooltip'; +import '../shared/components/pills/tracking'; +import './components/gl-commit-details'; +import './components/gl-wip-details'; +import './components/gl-inspect-nav'; +import './components/gl-status-nav'; -export type CommitState = SomeNonNullable, 'commit'>; -export class CommitDetailsApp extends App> { - constructor() { - super('CommitDetailsApp'); +export const uncommittedSha = '0000000000000000000000000000000000000000'; + +interface ExplainState { + cancelled?: boolean; + error?: { message: string }; + result?: { summary: string; body: string }; +} + +@customElement('gl-commit-details-app') +export class GlCommitDetailsApp extends GlAppHost> { + protected override createRenderRoot(): HTMLElement { + return this; + } + + protected override createStateProvider(state: Serialized, ipc: HostIpc): CommitDetailsStateProvider { + return new CommitDetailsStateProvider(this, state, ipc); + } + + protected override onPersistState(state: Serialized): void { + this._ipc.setPersistedState({ mode: state.mode, pinned: state.pinned, preferences: state.preferences }); + } + + @state() + private explain?: ExplainState; + + @state() + private generate?: GenerateState; + + @state() + private draftState: DraftState = { inReview: false }; + + @state() + private get isUncommitted(): boolean { + return this.state?.commit?.sha === uncommittedSha; + } + + @state() + private get isStash(): boolean { + return this.state?.commit?.stashNumber != null; + } + + private get wipStatus() { + const wip = this.state?.wip; + if (wip == null) return undefined; + + const branch = wip.branch; + if (branch == null) return undefined; + + const changes = wip.changes; + const working = changes?.files.length ?? 0; + const ahead = branch.tracking?.ahead ?? 0; + const behind = branch.tracking?.behind ?? 0; + const status = + behind > 0 && ahead > 0 + ? 'both' + : behind > 0 + ? 'behind' + : ahead > 0 + ? 'ahead' + : working > 0 + ? 'working' + : undefined; + + const branchName = wip.repositoryCount > 1 ? `${wip.repo.name}:${branch.name}` : branch.name; + + return { + branch: branchName, + upstream: branch.upstream?.name, + ahead: ahead, + behind: behind, + working: wip.changes?.files.length ?? 0, + status: status, + }; + } + + override connectedCallback(): void { + super.connectedCallback?.(); + + this.disposables.push( + DOM.on('gl-inspect-nav', 'gl-commit-actions', e => + this.onCommitActions(e), + ), + DOM.on('gl-status-nav', 'gl-branch-action', e => + this.onBranchAction(e.detail.action), + ), + DOM.on('[data-action="pick-commit"]', 'click', e => this.onPickCommit(e)), + DOM.on('[data-action="wip"]', 'click', e => this.onSwitchMode(e, 'wip')), + DOM.on('[data-action="details"]', 'click', e => this.onSwitchMode(e, 'commit')), + DOM.on('[data-action="search-commit"]', 'click', e => this.onSearchCommit(e)), + DOM.on('[data-action="files-layout"]', 'click', e => this.onToggleFilesLayout(e)), + DOM.on('gl-inspect-nav', 'gl-pin', () => this.onTogglePin()), + DOM.on('gl-inspect-nav', 'gl-back', () => this.onNavigate('back')), + DOM.on('gl-inspect-nav', 'gl-forward', () => this.onNavigate('forward')), + DOM.on('[data-action="create-patch"]', 'click', _e => this.onCreatePatchFromWip(true)), + DOM.on( + '[data-region="pullrequest-pane"]', + 'expanded-change', + e => this.onExpandedChange(e.detail, 'pullrequest'), + ), + DOM.on('[data-action="explain-commit"]', 'click', e => this.onExplainCommit(e)), + DOM.on('[data-action="switch-ai"]', 'click', e => this.onSwitchAiModel(e)), + DOM.on('gl-wip-details', 'create-patch', e => + this.onCreatePatchFromWip(e.detail.checked), + ), + + DOM.on('gl-commit-details', 'file-open-on-remote', e => + this.onOpenFileOnRemote(e.detail), + ), + DOM.on('gl-commit-details,gl-wip-details', 'file-open', e => + this.onOpenFile(e.detail), + ), + DOM.on('gl-commit-details', 'file-compare-working', e => + this.onCompareFileWithWorking(e.detail), + ), + DOM.on( + 'gl-commit-details,gl-wip-details', + 'file-compare-previous', + e => this.onCompareFileWithPrevious(e.detail), + ), + DOM.on('gl-commit-details', 'file-more-actions', e => + this.onFileMoreActions(e.detail), + ), + DOM.on('gl-wip-details', 'file-stage', e => + this.onStageFile(e.detail), + ), + DOM.on('gl-wip-details', 'file-unstage', e => + this.onUnstageFile(e.detail), + ), + DOM.on('gl-wip-details', 'data-action', e => + this.onBranchAction(e.detail.name), + ), + DOM.on('gl-wip-details', 'gl-inspect-create-suggestions', e => + this.onSuggestChanges(e.detail), + ), + DOM.on('gl-wip-details', 'gl-patch-generate-title', e => + this.onCreateGenerateTitle(e.detail), + ), + DOM.on('gl-wip-details', 'gl-show-code-suggestion', e => + this.onShowCodeSuggestion(e.detail), + ), + DOM.on('gl-wip-details', 'gl-patch-file-compare-previous', e => + this.onCompareFileWithPrevious(e.detail), + ), + DOM.on('gl-wip-details', 'gl-patch-file-open', e => + this.onOpenFile(e.detail), + ), + DOM.on('gl-wip-details', 'gl-patch-file-stage', e => + this.onStageFile(e.detail), + ), + DOM.on('gl-wip-details', 'gl-patch-file-unstage', e => + this.onUnstageFile(e.detail), + ), + DOM.on('gl-wip-details', 'gl-patch-create-cancelled', () => + this.onDraftStateChanged(false), + ), + DOM.on( + 'gl-status-nav,issue-pull-request', + 'gl-issue-pull-request-details', + () => this.onBranchAction('open-pr-details'), + ), + ); + } + + override updated(changedProperties: Map): void { + if (changedProperties.has('state')) { + this.updateDocumentProperties(); + if (this.state?.inReview != null && this.state.inReview !== this.draftState.inReview) { + this.draftState.inReview = this.state.inReview; + } + } + } + + private indentPreference = 16; + private updateDocumentProperties() { + const preference = this.state?.preferences?.indent; + if (preference === this.indentPreference) return; + this.indentPreference = preference ?? 16; + + const rootStyle = document.documentElement.style; + rootStyle.setProperty('--gitlens-tree-indent', `${this.indentPreference}px`); + } + + private onSuggestChanges(e: CreatePatchEventDetail) { + this._ipc.sendCommand(SuggestChangesCommand, e); + } + + private onShowCodeSuggestion(e: { id: string }) { + this._ipc.sendCommand(ShowCodeSuggestionCommand, e); + } + + private renderTopInspect() { + if (this.state?.commit == null) return nothing; + + return html``; + } + + private renderTopWip() { + if (this.state?.wip == null) return nothing; + + return html``; + } + + private renderRepoStatusContent(_isWip: boolean) { + const statusIndicator = this.wipStatus?.status; + return html` + + ${when( + this.wipStatus?.status != null, + () => + html``, + )} + ${when( + statusIndicator != null, + () => + html``, + )} + `; + // ${when( + // isWip !== true && statusIndicator != null, + // () => html``, + // )} + } + + private renderWipTooltipContent() { + if (this.wipStatus == null) return 'Overview'; + + return html` + Overview of  ${this.wipStatus.branch} + ${when( + this.wipStatus.status === 'both', + () => + html`
+ ${this.wipStatus!.branch} is + ${pluralize('commit', this.wipStatus!.behind)} behind and + ${pluralize('commit', this.wipStatus!.ahead)} ahead of + ${this.wipStatus!.upstream ?? 'origin'}`, + )} + ${when( + this.wipStatus.status === 'behind', + () => + html`
+ ${this.wipStatus!.branch} is + ${pluralize('commit', this.wipStatus!.behind)} behind + ${this.wipStatus!.upstream ?? 'origin'}`, + )} + ${when( + this.wipStatus.status === 'ahead', + () => + html`
+ ${this.wipStatus!.branch} is + ${pluralize('commit', this.wipStatus!.ahead)} ahead of + ${this.wipStatus!.upstream ?? 'origin'}`, + )} + ${when( + this.wipStatus.working > 0, + () => + html`
+ ${pluralize('working change', this.wipStatus!.working)}`, + )} + `; + } + + private renderTopSection() { + const isWip = this.state?.mode === 'wip'; + + return html` +
+ +
+ ${when( + this.state?.mode !== 'wip', + () => this.renderTopInspect(), + () => this.renderTopWip(), + )} +
+
+ `; + } + + override render(): unknown { + const wip = this.state?.wip; + + return html` +
+ ${this.renderTopSection()} +
+ ${when( + this.state?.mode === 'commit', + () => + html``, + () => + html`) => + this.onDraftStateChanged(e.detail.inReview)} + >`, + )} +
+
+ `; } - override onInitialize(): void { - const component = document.getElementById('app') as GlCommitDetailsApp; - component.state = this.state; - DOM.on>(component, 'state-changed', e => { - this.state = e.detail; - this.setState(this.state); + private onDraftStateChanged(inReview: boolean, silent = false) { + if (inReview === this.draftState.inReview) return; + this.draftState = { ...this.draftState, inReview: inReview }; + this.requestUpdate('draftState'); + + if (!silent) { + this._ipc.sendCommand(ChangeReviewModeCommand, { inReview: inReview }); + } + } + + private onBranchAction(name: string) { + switch (name) { + case 'pull': + this._ipc.sendCommand(PullCommand, undefined); + break; + case 'push': + this._ipc.sendCommand(PushCommand, undefined); + // this.onCommandClickedCore('gitlens.pushRepositories'); + break; + case 'fetch': + this._ipc.sendCommand(FetchCommand, undefined); + // this.onCommandClickedCore('gitlens.fetchRepositories'); + break; + case 'publish-branch': + this._ipc.sendCommand(PublishCommand, undefined); + // this.onCommandClickedCore('gitlens.publishRepository'); + break; + case 'switch': + this._ipc.sendCommand(SwitchCommand, undefined); + // this.onCommandClickedCore('gitlens.views.switchToBranch'); + break; + case 'open-pr-changes': + this._ipc.sendCommand(OpenPullRequestChangesCommand, undefined); + break; + case 'open-pr-compare': + this._ipc.sendCommand(OpenPullRequestComparisonCommand, undefined); + break; + case 'open-pr-remote': + this._ipc.sendCommand(OpenPullRequestOnRemoteCommand, undefined); + break; + case 'open-pr-details': + this._ipc.sendCommand(OpenPullRequestDetailsCommand, undefined); + break; + } + } + + private onCreatePatchFromWip(checked: boolean | 'staged' = true) { + if (this.state?.wip?.changes == null) return; + this._ipc.sendCommand(CreatePatchFromWipCommand, { changes: this.state.wip.changes, checked: checked }); + } + + private onCommandClickedCore(action?: GlCommands | `command:${GlCommands}`) { + const command = (action?.startsWith('command:') ? action.slice(8) : action) as GlCommands | undefined; + if (command == null) return; + + this._ipc.sendCommand(ExecuteCommand, { command: command }); + } + + private onSwitchAiModel(_e: MouseEvent) { + this.onCommandClickedCore('gitlens.ai.switchProvider'); + } + + private async onExplainCommit(_e: MouseEvent) { + try { + const result = await this._ipc.sendRequest(ExplainRequest, undefined); + if (result.error) { + this.explain = { error: { message: result.error.message ?? 'Error retrieving content' } }; + } else { + this.explain = result; + } + } catch (_ex) { + this.explain = { error: { message: 'Error retrieving content' } }; + } + } + + private async onCreateGenerateTitle(_e: CreatePatchMetadataEventDetail) { + try { + const result = await this._ipc.sendRequest(GenerateRequest, undefined); + + if (result.error) { + this.generate = { error: { message: result.error.message ?? 'Error retrieving content' } }; + } else if (result.title || result.description) { + this.generate = { + title: result.title, + description: result.description, + }; + // this.state = { + // ...this.state, + // create: { + // ...this.state.create!, + // title: result.title ?? this.state.create?.title, + // description: result.description ?? this.state.create?.description, + // }, + // }; + // this.setState(this.state); + } else { + this.generate = undefined; + } + } catch (_ex) { + this.generate = { error: { message: 'Error retrieving content' } }; + } + this.requestUpdate('generate'); + } + + private onToggleFilesLayout(e: MouseEvent) { + const layout = ((e.target as HTMLElement)?.dataset.filesLayout as ViewFilesLayout) ?? undefined; + if (layout === this.state?.preferences?.files?.layout) return; + + const files = { + ...this.state.preferences?.files, + layout: layout ?? 'auto', + }; + (this._stateProvider as CommitDetailsStateProvider).updatePreferences({ files: files }); + } + + private onExpandedChange(e: WebviewPaneExpandedChangeEventDetail, pane: string) { + let preferenceChange; + if (pane === 'pullrequest') { + preferenceChange = { pullRequestExpanded: e.expanded }; + } + if (preferenceChange == null) return; + + (this._stateProvider as CommitDetailsStateProvider).updatePreferences(preferenceChange); + } + + private onNavigate(direction: 'back' | 'forward') { + this._ipc.sendCommand(NavigateCommand, { direction: direction }); + } + + private onTogglePin() { + this._ipc.sendCommand(PinCommand, { pin: !this.state.pinned }); + } + + private onPickCommit(_e: MouseEvent) { + this._ipc.sendCommand(PickCommitCommand, undefined); + } + + private onSearchCommit(_e: MouseEvent) { + this._ipc.sendCommand(SearchCommitCommand, undefined); + } + + private onSwitchMode(_e: MouseEvent, mode: Mode) { + (this._stateProvider as CommitDetailsStateProvider).switchMode(mode); + } + + private onOpenFileOnRemote(e: FileChangeListItemDetail) { + this._ipc.sendCommand(OpenFileOnRemoteCommand, e); + } + + private onOpenFile(e: FileChangeListItemDetail) { + this._ipc.sendCommand(OpenFileCommand, e); + } + + private onCompareFileWithWorking(e: FileChangeListItemDetail) { + this._ipc.sendCommand(OpenFileCompareWorkingCommand, e); + } + + private onCompareFileWithPrevious(e: FileChangeListItemDetail) { + this._ipc.sendCommand(OpenFileComparePreviousCommand, e); + } + + private onFileMoreActions(e: FileChangeListItemDetail) { + this._ipc.sendCommand(ExecuteFileActionCommand, e); + } + + private onStageFile(e: FileChangeListItemDetail): void { + this._ipc.sendCommand(StageFileCommand, e); + } + + private onUnstageFile(e: FileChangeListItemDetail): void { + this._ipc.sendCommand(UnstageFileCommand, e); + } + + private onCommitActions(e: CustomEvent<{ action: string; alt: boolean }>) { + if (this.state?.commit === undefined) return; + + this._ipc.sendCommand(ExecuteCommitActionCommand, { + action: e.detail.action as ExecuteCommitActionsParams['action'], + alt: e.detail.alt, }); } } - -new CommitDetailsApp(); diff --git a/src/webviews/apps/commitDetails/components/commit-details-app.ts b/src/webviews/apps/commitDetails/components/commit-details-app.ts deleted file mode 100644 index e53bc50ea571e..0000000000000 --- a/src/webviews/apps/commitDetails/components/commit-details-app.ts +++ /dev/null @@ -1,728 +0,0 @@ -import { Badge, defineGkElement } from '@gitkraken/shared-web-components'; -import { html, LitElement, nothing } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { when } from 'lit/directives/when.js'; -import type { ViewFilesLayout } from '../../../../config'; -import type { GlCommands } from '../../../../constants.commands'; -import type { Serialized } from '../../../../system/serialize'; -import { pluralize } from '../../../../system/string'; -import type { DraftState, ExecuteCommitActionsParams, Mode, State } from '../../../commitDetails/protocol'; -import { - ChangeReviewModeCommand, - CreatePatchFromWipCommand, - DidChangeDraftStateNotification, - DidChangeHasAccountNotification, - DidChangeIntegrationsNotification, - DidChangeNotification, - DidChangeWipStateNotification, - ExecuteCommitActionCommand, - ExecuteFileActionCommand, - ExplainRequest, - FetchCommand, - GenerateRequest, - NavigateCommand, - OpenFileCommand, - OpenFileComparePreviousCommand, - OpenFileCompareWorkingCommand, - OpenFileOnRemoteCommand, - OpenPullRequestChangesCommand, - OpenPullRequestComparisonCommand, - OpenPullRequestDetailsCommand, - OpenPullRequestOnRemoteCommand, - PickCommitCommand, - PinCommand, - PublishCommand, - PullCommand, - PushCommand, - SearchCommitCommand, - ShowCodeSuggestionCommand, - StageFileCommand, - SuggestChangesCommand, - SwitchCommand, - SwitchModeCommand, - UnstageFileCommand, - UpdatePreferencesCommand, -} from '../../../commitDetails/protocol'; -import type { IpcMessage } from '../../../protocol'; -import { ExecuteCommand } from '../../../protocol'; -import type { CreatePatchMetadataEventDetail } from '../../plus/patchDetails/components/gl-patch-create'; -import type { IssuePullRequest } from '../../shared/components/rich/issue-pull-request'; -import type { WebviewPane, WebviewPaneExpandedChangeEventDetail } from '../../shared/components/webview-pane'; -import { DOM } from '../../shared/dom'; -import type { Disposable } from '../../shared/events'; -import { assertsSerialized, HostIpc } from '../../shared/ipc'; -import type { GlCommitDetails } from './gl-commit-details'; -import type { FileChangeListItemDetail } from './gl-details-base'; -import type { GlInspectNav } from './gl-inspect-nav'; -import type { CreatePatchEventDetail, GenerateState } from './gl-inspect-patch'; -import type { GlWipDetails } from './gl-wip-details'; -import '../../shared/components/code-icon'; -import '../../shared/components/indicators/indicator'; -import '../../shared/components/overlays/tooltip'; -import '../../shared/components/pills/tracking'; -import './gl-commit-details'; -import './gl-wip-details'; -import './gl-inspect-nav'; -import './gl-status-nav'; - -export const uncommittedSha = '0000000000000000000000000000000000000000'; - -interface ExplainState { - cancelled?: boolean; - error?: { message: string }; - result?: { summary: string; body: string }; -} - -@customElement('gl-commit-details-app') -export class GlCommitDetailsApp extends LitElement { - @property({ type: Object }) - state?: Serialized; - - @property({ type: Object }) - explain?: ExplainState; - - @property({ type: Object }) - generate?: GenerateState; - - @state() - draftState: DraftState = { inReview: false }; - - @state() - get isUncommitted(): boolean { - return this.state?.commit?.sha === uncommittedSha; - } - - get hasCommit(): boolean { - return this.state?.commit != null; - } - - @state() - get isStash(): boolean { - return this.state?.commit?.stashNumber != null; - } - - get wipStatus() { - const wip = this.state?.wip; - if (wip == null) return undefined; - - const branch = wip.branch; - if (branch == null) return undefined; - - const changes = wip.changes; - const working = changes?.files.length ?? 0; - const ahead = branch.tracking?.ahead ?? 0; - const behind = branch.tracking?.behind ?? 0; - const status = - behind > 0 && ahead > 0 - ? 'both' - : behind > 0 - ? 'behind' - : ahead > 0 - ? 'ahead' - : working > 0 - ? 'working' - : undefined; - - const branchName = wip.repositoryCount > 1 ? `${wip.repo.name}:${branch.name}` : branch.name; - - return { - branch: branchName, - upstream: branch.upstream?.name, - ahead: ahead, - behind: behind, - working: wip.changes?.files.length ?? 0, - status: status, - }; - } - - get navigation() { - if (this.state?.navigationStack == null) { - return { - back: false, - forward: false, - }; - } - - const actions = { - back: true, - forward: true, - }; - - if (this.state.navigationStack.count <= 1) { - actions.back = false; - actions.forward = false; - } else if (this.state.navigationStack.position === 0) { - actions.back = true; - actions.forward = false; - } else if (this.state.navigationStack.position === this.state.navigationStack.count - 1) { - actions.back = false; - actions.forward = true; - } - - return actions; - } - - private _disposables: Disposable[] = []; - private _hostIpc!: HostIpc; - - constructor() { - super(); - - defineGkElement(Badge); - } - - private indentPreference = 16; - private updateDocumentProperties() { - const preference = this.state?.preferences?.indent; - if (preference === this.indentPreference) return; - this.indentPreference = preference ?? 16; - - const rootStyle = document.documentElement.style; - rootStyle.setProperty('--gitlens-tree-indent', `${this.indentPreference}px`); - } - - override updated(changedProperties: Map): void { - if (changedProperties.has('state')) { - this.updateDocumentProperties(); - if (this.state?.inReview != null && this.state.inReview !== this.draftState.inReview) { - this.draftState.inReview = this.state.inReview; - } - } - } - - override connectedCallback(): void { - super.connectedCallback?.(); - - this._hostIpc = new HostIpc('commit-details'); - - this._disposables = [ - this._hostIpc.onReceiveMessage(e => this.onMessageReceived(e)), - this._hostIpc, - - DOM.on('gl-inspect-nav', 'gl-commit-actions', e => - this.onCommitActions(e), - ), - DOM.on('gl-status-nav', 'gl-branch-action', e => - this.onBranchAction(e.detail.action), - ), - DOM.on('[data-action="pick-commit"]', 'click', e => this.onPickCommit(e)), - DOM.on('[data-action="wip"]', 'click', e => this.onSwitchMode(e, 'wip')), - DOM.on('[data-action="details"]', 'click', e => this.onSwitchMode(e, 'commit')), - DOM.on('[data-action="search-commit"]', 'click', e => this.onSearchCommit(e)), - DOM.on('[data-action="files-layout"]', 'click', e => this.onToggleFilesLayout(e)), - DOM.on('gl-inspect-nav', 'gl-pin', () => this.onTogglePin()), - DOM.on('gl-inspect-nav', 'gl-back', () => this.onNavigate('back')), - DOM.on('gl-inspect-nav', 'gl-forward', () => this.onNavigate('forward')), - DOM.on('[data-action="create-patch"]', 'click', _e => this.onCreatePatchFromWip(true)), - DOM.on( - '[data-region="pullrequest-pane"]', - 'expanded-change', - e => this.onExpandedChange(e.detail, 'pullrequest'), - ), - DOM.on('[data-action="explain-commit"]', 'click', e => this.onExplainCommit(e)), - DOM.on('[data-action="switch-ai"]', 'click', e => this.onSwitchAiModel(e)), - DOM.on('gl-wip-details', 'create-patch', e => - this.onCreatePatchFromWip(e.detail.checked), - ), - - DOM.on('gl-commit-details', 'file-open-on-remote', e => - this.onOpenFileOnRemote(e.detail), - ), - DOM.on('gl-commit-details,gl-wip-details', 'file-open', e => - this.onOpenFile(e.detail), - ), - DOM.on('gl-commit-details', 'file-compare-working', e => - this.onCompareFileWithWorking(e.detail), - ), - DOM.on( - 'gl-commit-details,gl-wip-details', - 'file-compare-previous', - e => this.onCompareFileWithPrevious(e.detail), - ), - DOM.on('gl-commit-details', 'file-more-actions', e => - this.onFileMoreActions(e.detail), - ), - DOM.on('gl-wip-details', 'file-stage', e => - this.onStageFile(e.detail), - ), - DOM.on('gl-wip-details', 'file-unstage', e => - this.onUnstageFile(e.detail), - ), - DOM.on('gl-wip-details', 'data-action', e => - this.onBranchAction(e.detail.name), - ), - DOM.on('gl-wip-details', 'gl-inspect-create-suggestions', e => - this.onSuggestChanges(e.detail), - ), - DOM.on('gl-wip-details', 'gl-patch-generate-title', e => - this.onCreateGenerateTitle(e.detail), - ), - DOM.on('gl-wip-details', 'gl-show-code-suggestion', e => - this.onShowCodeSuggestion(e.detail), - ), - DOM.on('gl-wip-details', 'gl-patch-file-compare-previous', e => - this.onCompareFileWithPrevious(e.detail), - ), - DOM.on('gl-wip-details', 'gl-patch-file-open', e => - this.onOpenFile(e.detail), - ), - DOM.on('gl-wip-details', 'gl-patch-file-stage', e => - this.onStageFile(e.detail), - ), - DOM.on('gl-wip-details', 'gl-patch-file-unstage', e => - this.onUnstageFile(e.detail), - ), - DOM.on('gl-wip-details', 'gl-patch-create-cancelled', () => - this.onDraftStateChanged(false), - ), - DOM.on( - 'gl-status-nav,issue-pull-request', - 'gl-issue-pull-request-details', - () => this.onBranchAction('open-pr-details'), - ), - ]; - } - - private onSuggestChanges(e: CreatePatchEventDetail) { - this._hostIpc.sendCommand(SuggestChangesCommand, e); - } - - private onShowCodeSuggestion(e: { id: string }) { - this._hostIpc.sendCommand(ShowCodeSuggestionCommand, e); - } - - private onMessageReceived(msg: IpcMessage) { - switch (true) { - // case DidChangeRichStateNotificationType.method: - // onIpc(DidChangeRichStateNotificationType, msg, params => { - // if (this.state.selected == null) return; - - // assertsSerialized(params); - - // const newState = { ...this.state }; - // if (params.formattedMessage != null) { - // newState.selected!.message = params.formattedMessage; - // } - // // if (params.pullRequest != null) { - // newState.pullRequest = params.pullRequest; - // // } - // // if (params.formattedMessage != null) { - // newState.autolinkedIssues = params.autolinkedIssues; - // // } - - // this.state = newState; - // this.setState(this.state); - - // this.renderRichContent(); - // }); - // break; - case DidChangeNotification.is(msg): - assertsSerialized(msg.params.state); - - this.state = msg.params.state; - this.dispatchEvent(new CustomEvent('state-changed', { detail: this.state })); - // this.setState(this.state); - // this.attachState(); - break; - - case DidChangeWipStateNotification.is(msg): - this.state = { ...this.state!, wip: msg.params.wip, inReview: msg.params.inReview }; - this.dispatchEvent(new CustomEvent('state-changed', { detail: this.state })); - // this.setState(this.state); - // this.attachState(); - break; - case DidChangeDraftStateNotification.is(msg): - this.onDraftStateChanged(msg.params.inReview, true); - break; - case DidChangeHasAccountNotification.is(msg): - this.state = { ...this.state!, hasAccount: msg.params.hasAccount }; - this.dispatchEvent(new CustomEvent('state-changed', { detail: this.state })); - break; - case DidChangeIntegrationsNotification.is(msg): - this.state = { ...this.state!, hasIntegrationsConnected: msg.params.hasIntegrationsConnected }; - this.dispatchEvent(new CustomEvent('state-changed', { detail: this.state })); - break; - } - } - - override disconnectedCallback(): void { - this._disposables.forEach(d => d.dispose()); - this._disposables = []; - - super.disconnectedCallback?.(); - } - - private renderTopInspect() { - if (this.state?.commit == null) return nothing; - - return html``; - } - - private renderTopWip() { - if (this.state?.wip == null) return nothing; - - return html``; - } - - private renderRepoStatusContent(_isWip: boolean) { - const statusIndicator = this.wipStatus?.status; - return html` - - ${when( - this.wipStatus?.status != null, - () => - html``, - )} - ${when( - statusIndicator != null, - () => - html``, - )} - `; - // ${when( - // isWip !== true && statusIndicator != null, - // () => html``, - // )} - } - - private renderWipTooltipContent() { - if (this.wipStatus == null) return 'Overview'; - - return html` - Overview of  ${this.wipStatus.branch} - ${when( - this.wipStatus.status === 'both', - () => - html`
- ${this.wipStatus!.branch} is - ${pluralize('commit', this.wipStatus!.behind)} behind and - ${pluralize('commit', this.wipStatus!.ahead)} ahead of - ${this.wipStatus!.upstream ?? 'origin'}`, - )} - ${when( - this.wipStatus.status === 'behind', - () => - html`
- ${this.wipStatus!.branch} is - ${pluralize('commit', this.wipStatus!.behind)} behind - ${this.wipStatus!.upstream ?? 'origin'}`, - )} - ${when( - this.wipStatus.status === 'ahead', - () => - html`
- ${this.wipStatus!.branch} is - ${pluralize('commit', this.wipStatus!.ahead)} ahead of - ${this.wipStatus!.upstream ?? 'origin'}`, - )} - ${when( - this.wipStatus.working > 0, - () => - html`
- ${pluralize('working change', this.wipStatus!.working)}`, - )} - `; - } - - private renderTopSection() { - const isWip = this.state?.mode === 'wip'; - - return html` -
- -
- ${when( - this.state?.mode !== 'wip', - () => this.renderTopInspect(), - () => this.renderTopWip(), - )} -
-
- `; - } - - override render(): unknown { - const wip = this.state?.wip; - - return html` -
- ${this.renderTopSection()} -
- ${when( - this.state?.mode === 'commit', - () => - html``, - () => - html`) => - this.onDraftStateChanged(e.detail.inReview)} - >`, - )} -
-
- `; - } - - protected override createRenderRoot(): HTMLElement { - return this; - } - - private onDraftStateChanged(inReview: boolean, silent = false) { - if (inReview === this.draftState.inReview) return; - this.draftState = { ...this.draftState, inReview: inReview }; - this.requestUpdate('draftState'); - - if (!silent) { - this._hostIpc.sendCommand(ChangeReviewModeCommand, { inReview: inReview }); - } - } - - private onBranchAction(name: string) { - switch (name) { - case 'pull': - this._hostIpc.sendCommand(PullCommand, undefined); - break; - case 'push': - this._hostIpc.sendCommand(PushCommand, undefined); - // this.onCommandClickedCore('gitlens.pushRepositories'); - break; - case 'fetch': - this._hostIpc.sendCommand(FetchCommand, undefined); - // this.onCommandClickedCore('gitlens.fetchRepositories'); - break; - case 'publish-branch': - this._hostIpc.sendCommand(PublishCommand, undefined); - // this.onCommandClickedCore('gitlens.publishRepository'); - break; - case 'switch': - this._hostIpc.sendCommand(SwitchCommand, undefined); - // this.onCommandClickedCore('gitlens.views.switchToBranch'); - break; - case 'open-pr-changes': - this._hostIpc.sendCommand(OpenPullRequestChangesCommand, undefined); - break; - case 'open-pr-compare': - this._hostIpc.sendCommand(OpenPullRequestComparisonCommand, undefined); - break; - case 'open-pr-remote': - this._hostIpc.sendCommand(OpenPullRequestOnRemoteCommand, undefined); - break; - case 'open-pr-details': - this._hostIpc.sendCommand(OpenPullRequestDetailsCommand, undefined); - break; - } - } - - private onCreatePatchFromWip(checked: boolean | 'staged' = true) { - if (this.state?.wip?.changes == null) return; - this._hostIpc.sendCommand(CreatePatchFromWipCommand, { changes: this.state.wip.changes, checked: checked }); - } - - private onCommandClickedCore(action?: GlCommands | `command:${GlCommands}`) { - const command = (action?.startsWith('command:') ? action.slice(8) : action) as GlCommands | undefined; - if (command == null) return; - - this._hostIpc.sendCommand(ExecuteCommand, { command: command }); - } - - private onSwitchAiModel(_e: MouseEvent) { - this.onCommandClickedCore('gitlens.ai.switchProvider'); - } - - private async onExplainCommit(_e: MouseEvent) { - try { - const result = await this._hostIpc.sendRequest(ExplainRequest, undefined); - if (result.error) { - this.explain = { error: { message: result.error.message ?? 'Error retrieving content' } }; - } else { - this.explain = result; - } - } catch (_ex) { - this.explain = { error: { message: 'Error retrieving content' } }; - } - } - - private async onCreateGenerateTitle(_e: CreatePatchMetadataEventDetail) { - try { - const result = await this._hostIpc.sendRequest(GenerateRequest, undefined); - - if (result.error) { - this.generate = { error: { message: result.error.message ?? 'Error retrieving content' } }; - } else if (result.title || result.description) { - this.generate = { - title: result.title, - description: result.description, - }; - // this.state = { - // ...this.state, - // create: { - // ...this.state.create!, - // title: result.title ?? this.state.create?.title, - // description: result.description ?? this.state.create?.description, - // }, - // }; - // this.setState(this.state); - } else { - this.generate = undefined; - } - } catch (_ex) { - this.generate = { error: { message: 'Error retrieving content' } }; - } - this.requestUpdate('generate'); - } - - private onToggleFilesLayout(e: MouseEvent) { - const layout = ((e.target as HTMLElement)?.dataset.filesLayout as ViewFilesLayout) ?? undefined; - if (layout === this.state?.preferences?.files?.layout) return; - - const files = { - ...this.state!.preferences?.files, - layout: layout ?? 'auto', - }; - - this.state = { ...this.state, preferences: { ...this.state!.preferences, files: files } } as any; - // this.attachState(); - - this._hostIpc.sendCommand(UpdatePreferencesCommand, { files: files }); - } - - private onExpandedChange(e: WebviewPaneExpandedChangeEventDetail, pane: string) { - let preferenceChange; - if (pane === 'pullrequest') { - preferenceChange = { pullRequestExpanded: e.expanded }; - } - if (preferenceChange == null) return; - - this.state = { - ...this.state, - preferences: { ...this.state!.preferences, ...preferenceChange }, - } as any; - // this.attachState(); - - this._hostIpc.sendCommand(UpdatePreferencesCommand, preferenceChange); - } - - private onNavigate(direction: 'back' | 'forward') { - this._hostIpc.sendCommand(NavigateCommand, { direction: direction }); - } - - private onTogglePin() { - this._hostIpc.sendCommand(PinCommand, { pin: !this.state!.pinned }); - } - - private onPickCommit(_e: MouseEvent) { - this._hostIpc.sendCommand(PickCommitCommand, undefined); - } - - private onSearchCommit(_e: MouseEvent) { - this._hostIpc.sendCommand(SearchCommitCommand, undefined); - } - - private onSwitchMode(_e: MouseEvent, mode: Mode) { - this.state = { ...this.state, mode: mode } as any; - // this.attachState(); - - this._hostIpc.sendCommand(SwitchModeCommand, { mode: mode, repoPath: this.state!.commit?.repoPath }); - } - - private onOpenFileOnRemote(e: FileChangeListItemDetail) { - this._hostIpc.sendCommand(OpenFileOnRemoteCommand, e); - } - - private onOpenFile(e: FileChangeListItemDetail) { - this._hostIpc.sendCommand(OpenFileCommand, e); - } - - private onCompareFileWithWorking(e: FileChangeListItemDetail) { - this._hostIpc.sendCommand(OpenFileCompareWorkingCommand, e); - } - - private onCompareFileWithPrevious(e: FileChangeListItemDetail) { - this._hostIpc.sendCommand(OpenFileComparePreviousCommand, e); - } - - private onFileMoreActions(e: FileChangeListItemDetail) { - this._hostIpc.sendCommand(ExecuteFileActionCommand, e); - } - - private onStageFile(e: FileChangeListItemDetail): void { - this._hostIpc.sendCommand(StageFileCommand, e); - } - - private onUnstageFile(e: FileChangeListItemDetail): void { - this._hostIpc.sendCommand(UnstageFileCommand, e); - } - - private onCommitActions(e: CustomEvent<{ action: string; alt: boolean }>) { - if (this.state?.commit === undefined) { - return; - } - - this._hostIpc.sendCommand(ExecuteCommitActionCommand, { - action: e.detail.action as ExecuteCommitActionsParams['action'], - alt: e.detail.alt, - }); - } -} diff --git a/src/webviews/apps/commitDetails/components/gl-commit-details.ts b/src/webviews/apps/commitDetails/components/gl-commit-details.ts index 24892405f262a..387321d381947 100644 --- a/src/webviews/apps/commitDetails/components/gl-commit-details.ts +++ b/src/webviews/apps/commitDetails/components/gl-commit-details.ts @@ -11,7 +11,7 @@ import type { Serialized } from '../../../../system/serialize'; import type { State } from '../../../commitDetails/protocol'; import { messageHeadlineSplitterToken } from '../../../commitDetails/protocol'; import type { TreeItemAction, TreeItemBase } from '../../shared/components/tree/base'; -import { uncommittedSha } from './commit-details-app'; +import { uncommittedSha } from '../commitDetails'; import type { File } from './gl-details-base'; import { GlDetailsBase } from './gl-details-base'; import '../../shared/components/button'; diff --git a/src/webviews/apps/commitDetails/context.ts b/src/webviews/apps/commitDetails/context.ts new file mode 100644 index 0000000000000..d96e0238a3946 --- /dev/null +++ b/src/webviews/apps/commitDetails/context.ts @@ -0,0 +1,5 @@ +import { createContext } from '@lit/context'; +import type { Serialized } from '../../../system/serialize'; +import type { State } from '../../commitDetails/protocol'; + +export const stateContext = createContext>('state'); diff --git a/src/webviews/apps/commitDetails/stateProvider.ts b/src/webviews/apps/commitDetails/stateProvider.ts new file mode 100644 index 0000000000000..a89599b3e1554 --- /dev/null +++ b/src/webviews/apps/commitDetails/stateProvider.ts @@ -0,0 +1,128 @@ +import { ContextProvider } from '@lit/context'; +import type { Serialized } from '../../../system/serialize'; +import type { State, UpdateablePreferences } from '../../commitDetails/protocol'; +import { + ChangeReviewModeCommand, + DidChangeDraftStateNotification, + DidChangeHasAccountNotification, + DidChangeIntegrationsNotification, + DidChangeNotification, + DidChangeWipStateNotification, + SwitchModeCommand, + UpdatePreferencesCommand, +} from '../../commitDetails/protocol'; +import type { ReactiveElementHost, StateProvider } from '../shared/appHost'; +import type { Disposable } from '../shared/events'; +import type { HostIpc } from '../shared/ipc'; +import { assertsSerialized } from '../shared/ipc'; +import { stateContext } from './context'; + +export class CommitDetailsStateProvider implements StateProvider> { + private readonly disposable: Disposable; + private readonly provider: ContextProvider<{ __context__: Serialized }, ReactiveElementHost>; + + private _state: Serialized; + get state(): Serialized { + return this._state; + } + + private _host: ReactiveElementHost; + + constructor( + host: ReactiveElementHost, + state: Serialized, + private readonly _ipc: HostIpc, + ) { + this._host = host; + this._state = state; + this.provider = new ContextProvider(host, { context: stateContext, initialValue: state }); + + this.disposable = this._ipc.onReceiveMessage(msg => { + switch (true) { + // case DidChangeRichStateNotificationType.method: + // onIpc(DidChangeRichStateNotificationType, msg, params => { + // if (this._state.selected == null) return; + + // assertsSerialized(params); + + // const newState = { ...this._state }; + // if (params.formattedMessage != null) { + // newState.selected!.message = params.formattedMessage; + // } + // // if (params.pullRequest != null) { + // newState.pullRequest = params.pullRequest; + // // } + // // if (params.formattedMessage != null) { + // newState.autolinkedIssues = params.autolinkedIssues; + // // } + + // this._state = newState; + // this.provider.setValue(this._state, true); + + // this.renderRichContent(); + // }); + // break; + + case DidChangeNotification.is(msg): + assertsSerialized(msg.params.state); + + this._state = { ...msg.params.state, timestamp: Date.now() }; + this.provider.setValue(this._state, true); + host.requestUpdate(); + break; + + case DidChangeWipStateNotification.is(msg): + this._state = { ...this._state, wip: msg.params.wip, inReview: msg.params.inReview }; + this.provider.setValue(this._state, true); + host.requestUpdate(); + break; + + case DidChangeDraftStateNotification.is(msg): + this.onDraftStateChanged(host, msg.params.inReview, true); + break; + + case DidChangeHasAccountNotification.is(msg): + this._state = { ...this._state, hasAccount: msg.params.hasAccount }; + this.provider.setValue(this._state, true); + host.requestUpdate(); + break; + + case DidChangeIntegrationsNotification.is(msg): + this._state = { ...this._state, hasIntegrationsConnected: msg.params.hasIntegrationsConnected }; + this.provider.setValue(this._state, true); + host.requestUpdate(); + break; + } + }); + } + + dispose(): void { + this.disposable.dispose(); + } + + private onDraftStateChanged(host: ReactiveElementHost, inReview: boolean, silent = false) { + if (inReview === this._state.inReview) return; + this._state = { ...this._state, inReview: inReview }; + this.provider.setValue(this._state, true); + host.requestUpdate(); + if (!silent) { + this._ipc.sendCommand(ChangeReviewModeCommand, { inReview: inReview }); + } + } + + switchMode(mode: State['mode']) { + this._state = { ...this._state, mode: mode }; + this.provider.setValue(this._state, true); + this._host.requestUpdate(); + + this._ipc.sendCommand(SwitchModeCommand, { mode: mode, repoPath: this._state.commit?.repoPath }); + } + + updatePreferences(preferenceChange: UpdateablePreferences) { + this._state = { ...this._state, preferences: { ...this._state.preferences, ...preferenceChange } }; + this.provider.setValue(this._state, true); + this._host.requestUpdate(); + + this._ipc.sendCommand(UpdatePreferencesCommand, preferenceChange); + } +} diff --git a/src/webviews/apps/plus/patchDetails/components/patch-details-app.ts b/src/webviews/apps/plus/patchDetails/components/patch-details-app.ts index c8038318d392e..7327576e9fcdb 100644 --- a/src/webviews/apps/plus/patchDetails/components/patch-details-app.ts +++ b/src/webviews/apps/plus/patchDetails/components/patch-details-app.ts @@ -1,4 +1,4 @@ -import { Badge, defineGkElement, Menu, MenuItem, Popover } from '@gitkraken/shared-web-components'; +import { defineGkElement, Menu, MenuItem, Popover } from '@gitkraken/shared-web-components'; import { html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; @@ -61,7 +61,7 @@ export class GlPatchDetailsApp extends GlElement { constructor() { super(); - defineGkElement(Badge, Popover, Menu, MenuItem); + defineGkElement(Popover, Menu, MenuItem); } get wipChangesCount(): number { diff --git a/src/webviews/apps/shared/styles/details-base.scss b/src/webviews/apps/shared/styles/details-base.scss index 3605800be1f87..9c7cec8afa35e 100644 --- a/src/webviews/apps/shared/styles/details-base.scss +++ b/src/webviews/apps/shared/styles/details-base.scss @@ -38,9 +38,6 @@ html { } body { - --gk-badge-outline-color: var(--vscode-badge-foreground); - --gk-badge-filled-background-color: var(--vscode-badge-background); - --gk-badge-filled-color: var(--vscode-badge-foreground); font-family: var(--font-family); font-size: var(--font-size); color: var(--color-foreground);