Parcourir la source

Add file tree mode in commit details

main
Keith Daulton il y a 2 ans
Parent
révision
d4662f355d
12 fichiers modifiés avec 749 ajouts et 61 suppressions
  1. +1
    -0
      package.json
  2. +1
    -0
      src/storage.ts
  3. +7
    -0
      src/webviews/apps/commitDetails/commitDetails.html
  4. +69
    -8
      src/webviews/apps/commitDetails/commitDetails.scss
  5. +141
    -36
      src/webviews/apps/commitDetails/commitDetails.ts
  6. +15
    -10
      src/webviews/apps/shared/components/commit/file-change-item.ts
  7. +10
    -0
      src/webviews/apps/shared/components/converters/number-converter.ts
  8. +206
    -0
      src/webviews/apps/shared/components/list/file-change-list-item.ts
  9. +71
    -0
      src/webviews/apps/shared/components/list/list-container.ts
  10. +206
    -0
      src/webviews/apps/shared/components/list/list-item.ts
  11. +19
    -6
      src/webviews/commitDetails/commitDetailsWebviewView.ts
  12. +3
    -1
      src/webviews/commitDetails/protocol.ts

+ 1
- 0
package.json Voir le fichier

@ -11681,6 +11681,7 @@
},
"dependencies": {
"@gitkraken/gitkraken-components": "1.0.0-rc.13",
"@microsoft/fast-element": "^1.10.5",
"@octokit/core": "4.0.5",
"@vscode/codicons": "0.0.32",
"@vscode/webview-ui-toolkit": "1.0.1",

+ 1
- 0
src/storage.ts Voir le fichier

@ -182,6 +182,7 @@ export interface WorkspaceStorage {
};
commitDetails: {
autolinksExpanded?: boolean;
filesAsTree?: boolean;
};
};

+ 7
- 0
src/webviews/apps/commitDetails/commitDetails.html Voir le fichier

@ -115,6 +115,13 @@
<webview-pane class="commit-details__files" collapsable expanded>
<span slot="title">Files changed </span>
<span slot="subtitle" data-region="stats"></span>
<div style="text-align: center;">
<div class="switch">
<button type="button" class="switch__option" data-switch-value="list"><code-icon icon="list-flat"></code-icon> Path</button>
<button type="button" class="switch__option" data-switch-value="list-tree"><code-icon icon="list-tree"></code-icon> Tree</button>
</div>
</div>
<ul class="change-list" data-region="files">
<li class="change-list__item commit-details__item-skeleton">
<skeleton-loader></skeleton-loader>

+ 69
- 8
src/webviews/apps/commitDetails/commitDetails.scss Voir le fichier

@ -6,16 +6,14 @@
// generic resets
html {
font-size: 62.5%;
box-sizing: border-box;
// box-sizing: border-box;
font-family: var(--font-family);
}
* {
&,
&::before,
&::after {
box-sizing: inherit;
}
*,
*:before,
*:after {
box-sizing: border-box;
}
body {
@ -100,6 +98,64 @@ ul {
max-width: 30rem;
}
.switch {
margin-left: auto;
display: inline-flex;
flex-direction: row;
border-radius: 0.25em;
gap: 0.1rem;
.vscode-dark & {
background-color: var(--color-background--lighten-075);
}
.vscode-light & {
background-color: var(--color-background--darken-075);
}
&__option {
display: inline-flex;
justify-content: center;
align-items: flex-end;
border-radius: 0.25em;
color: inherit;
padding: 0.2rem 0.8rem;
text-decoration: none;
background: none;
border: none;
cursor: pointer;
> * {
pointer-events: none;
}
&:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
&:hover {
color: var(--vscode-foreground);
text-decoration: none;
.vscode-dark & {
background-color: var(--color-background--lighten-10);
}
.vscode-light & {
background-color: var(--color-background--darken-10);
}
}
&.is-selected {
color: var(--vscode-foreground);
.vscode-dark & {
background-color: var(--color-background--lighten-15);
}
.vscode-light & {
background-color: var(--color-background--darken-15);
}
}
}
}
@media (min-width: 640px) {
.button-container {
max-width: 100%;
@ -279,12 +335,15 @@ ul {
}
&__file {
--tree-level: 1;
padding: {
left: var(--gitlens-gutter-width);
left: calc(var(--gitlens-gutter-width) * var(--tree-level));
right: var(--gitlens-scrollbar-gutter-width);
top: 1px;
bottom: 1px;
}
line-height: 22px;
height: 22px;
}
&__item-skeleton {
padding: {
@ -323,3 +382,5 @@ ul {
@import '../shared/codicons';
@import '../shared/glicons';

+ 141
- 36
src/webviews/apps/commitDetails/commitDetails.ts Voir le fichier

@ -1,4 +1,6 @@
/*global*/
import type { HierarchicalItem } from '../../../system/array';
import { makeHierarchical } from '../../../system/array';
import type { Serialized } from '../../../system/serialize';
import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
@ -18,7 +20,7 @@ import {
SearchCommitCommandType,
} from '../../commitDetails/protocol';
import { App } from '../shared/appBase';
import type { FileChangeItem, FileChangeItemEventDetail } from '../shared/components/commit/file-change-item';
import type { FileChangeListItem, FileChangeListItemDetail } from '../shared/components/list/file-change-list-item';
import type { WebviewPane, WebviewPaneExpandedChangeEventDetail } from '../shared/components/webview-pane';
import { DOM } from '../shared/dom';
import './commitDetails.scss';
@ -28,8 +30,10 @@ import '../shared/components/formatted-date';
import '../shared/components/rich/issue-pull-request';
import '../shared/components/skeleton-loader';
import '../shared/components/commit/commit-stats';
import '../shared/components/commit/file-change-item';
import '../shared/components/webview-pane';
import '../shared/components/list/list-container';
import '../shared/components/list/list-item';
import '../shared/components/list/file-change-list-item';
const uncommittedSha = '0000000000000000000000000000000000000000';
@ -48,38 +52,26 @@ export class CommitDetailsApp extends App> {
override onBind() {
const disposables = [
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-open-on-remote', e =>
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-open-on-remote', e =>
this.onOpenFileOnRemote(e.detail),
),
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-open', e =>
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-open', e =>
this.onOpenFile(e.detail),
),
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-compare-working', e =>
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-compare-working', e =>
this.onCompareFileWithWorking(e.detail),
),
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-compare-previous', e =>
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-compare-previous', e =>
this.onCompareFileWithPrevious(e.detail),
),
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-more-actions', e =>
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-more-actions', e =>
this.onFileMoreActions(e.detail),
),
DOM.on('[data-action="commit-actions"]', 'click', e => this.onCommitActions(e)),
DOM.on('[data-action="pick-commit"]', 'click', e => this.onPickCommit(e)),
DOM.on('[data-action="search-commit"]', 'click', e => this.onSearchCommit(e)),
DOM.on('[data-action="autolink-settings"]', 'click', e => this.onAutolinkSettings(e)),
DOM.on('file-change-item', 'keydown', (e, target: HTMLElement) => {
if (e.key === 'Enter' || e.key === ' ') {
(target as FileChangeItem).open(e.key === 'Enter' ? { preserveFocus: false } : undefined);
} else if (e.key === 'ArrowUp') {
const $previous: HTMLElement | null = target.parentElement?.previousElementSibling
?.firstElementChild as HTMLElement;
$previous?.focus();
} else if (e.key === 'ArrowDown') {
const $next: HTMLElement | null = target.parentElement?.nextElementSibling
?.firstElementChild as HTMLElement;
$next?.focus();
}
}),
DOM.on('[data-switch-value]', 'click', e => this.onTreeSetting(e)),
DOM.on('[data-action="pin"]', 'click', e => this.onTogglePin(e)),
DOM.on<WebviewPane, WebviewPaneExpandedChangeEventDetail>(
'[data-region="rich-pane"]',
@ -127,6 +119,22 @@ export class CommitDetailsApp extends App> {
}
}
private onTreeSetting(e: MouseEvent) {
const isTree = (e.target as HTMLElement)?.getAttribute('data-switch-value') === 'list-tree';
if (isTree === this.state.preferences?.filesAsTree) {
return;
}
this.state.preferences = { ...this.state.preferences, filesAsTree: isTree };
this.renderFiles(this.state as CommitState);
this.sendCommand(PreferencesCommandType, {
filesAsTree: isTree,
});
}
private onExpandedChange(e: WebviewPaneExpandedChangeEventDetail) {
this.sendCommand(PreferencesCommandType, {
autolinksExpanded: e.expanded,
@ -151,23 +159,23 @@ export class CommitDetailsApp extends App> {
this.sendCommand(PickCommitCommandType, undefined);
}
private onOpenFileOnRemote(e: FileChangeItemEventDetail) {
private onOpenFileOnRemote(e: FileChangeListItemDetail) {
this.sendCommand(OpenFileOnRemoteCommandType, e);
}
private onOpenFile(e: FileChangeItemEventDetail) {
private onOpenFile(e: FileChangeListItemDetail) {
this.sendCommand(OpenFileCommandType, e);
}
private onCompareFileWithWorking(e: FileChangeItemEventDetail) {
private onCompareFileWithWorking(e: FileChangeListItemDetail) {
this.sendCommand(OpenFileCompareWorkingCommandType, e);
}
private onCompareFileWithPrevious(e: FileChangeItemEventDetail) {
private onCompareFileWithPrevious(e: FileChangeListItemDetail) {
this.sendCommand(OpenFileComparePreviousCommandType, e);
}
private onFileMoreActions(e: FileChangeItemEventDetail) {
private onFileMoreActions(e: FileChangeListItemDetail) {
this.sendCommand(FileActionsCommandType, e);
}
@ -221,7 +229,9 @@ export class CommitDetailsApp extends App> {
renderActions(state: CommitState) {
const isHidden = state.selected?.sha === uncommittedSha ? 'true' : 'false';
[...document.querySelectorAll('[data-action-type="graph"],[data-action-type="more"]')].forEach($el => $el.setAttribute('aria-hidden', isHidden));
[...document.querySelectorAll('[data-action-type="graph"],[data-action-type="more"]')].forEach($el =>
$el.setAttribute('aria-hidden', isHidden),
);
}
renderPin(state: CommitState) {
@ -328,16 +338,76 @@ export class CommitDetailsApp extends App> {
}
if (state.selected.files?.length) {
const stashAttr = state.selected.isStash ? 'stash ' : state.selected.sha === uncommittedSha ? 'uncommitted ' : '';
$el.innerHTML = state.selected.files
.map(
(file: Record<string, any>) => `
<li class="change-list__item">
<file-change-item class="commit-details__file" ${stashAttr}status="${file.status}" path="${file.path}" repo-path="${file.repoPath}" icon="${file.icon.dark}"></file-change-item>
</li>
`,
)
.join('');
const isTree = state.preferences?.filesAsTree === true;
document.querySelector('[data-switch-value="list"]')?.classList.toggle('is-selected', !isTree);
document.querySelector('[data-switch-value="list-tree"]')?.classList.toggle('is-selected', isTree);
const stashAttr = state.selected.isStash
? 'stash '
: state.selected.sha === uncommittedSha
? 'uncommitted '
: '';
if (isTree) {
const tree = makeHierarchical(
state.selected.files,
n => n.path.split('/'),
(...parts: string[]) => parts.join('/'),
true,
);
const flatTree = flattenHeirarchy(tree);
$el.innerHTML = `
<li class="change-list__item">
<list-container>
${flatTree
.map(({ level, item }) => {
if (item.name === '') {
return '';
}
if (item.value == null) {
return `
<list-item level="${level}" tree branch>
<code-icon slot="icon" icon="folder" title="Directory" aria-label="Directory"></code-icon>
${item.name}
</list-item>
`;
}
return `
<file-change-list-item
tree
level="${level}"
${stashAttr}
path="${item.value.path}"
icon="${item.value.icon.dark}"
status="${item.value.status}"
></file-change-list-item>
`;
})
.join('')}
</list-container>
</li>`;
} else {
$el.innerHTML = `
<li class="change-list__item">
<list-container>
${state.selected.files
.map(
(file: Record<string, any>) => `
<file-change-list-item
${stashAttr}
path="${file.path}"
icon="${file.icon.dark}"
status="${file.status}"
></file-change-list-item>
`,
)
.join('')}
</list-container>
</li>`;
}
$el.setAttribute('aria-hidden', 'false');
} else {
$el.innerHTML = '';
@ -509,4 +579,39 @@ export class CommitDetailsApp extends App> {
function assertsSerialized<T>(obj: unknown): asserts obj is Serialized<T> {}
function flattenHeirarchy<T>(item: HierarchicalItem<T>, level = 0): { level: number; item: HierarchicalItem<T> }[] {
const flattened: { level: number; item: HierarchicalItem<T> }[] = [];
if (item == null) {
return flattened;
}
flattened.push({ level: level, item: item });
if (item.children != null) {
const children = Array.from(item.children.values());
children.sort((a, b) => {
if (!a.value || !b.value) {
return (a.value ? 1 : -1) - (b.value ? 1 : -1);
}
if (a.relativePath < b.relativePath) {
return -1;
}
if (a.relativePath > b.relativePath) {
return 1;
}
return 0;
});
children.forEach(child => {
flattened.push(...flattenHeirarchy(child, level + 1));
});
}
return flattened;
}
new CommitDetailsApp();

+ 15
- 10
src/webviews/apps/shared/components/commit/file-change-item.ts Voir le fichier

@ -162,6 +162,9 @@ export class FileChangeItem extends LitElement {
@property({ type: Boolean, reflect: true })
uncommitted = false;
@property({ type: Boolean, reflect: true })
tree = false;
private renderIcon() {
if (this.icon !== '') {
return html`<img class="change-list__status-icon" src="${this.icon}" />`;
@ -183,14 +186,14 @@ export class FileChangeItem extends LitElement {
const statusName = this.status !== '' ? statusTextMap[this.status] : '';
const pathIndex = this.path.lastIndexOf('/');
const fileName = pathIndex > -1 ? this.path.substring(pathIndex + 1) : this.path;
const filePath = pathIndex > -1 ? this.path.substring(0, pathIndex) : '';
const filePath = !this.tree && pathIndex > -1 ? this.path.substring(0, pathIndex) : '';
return html`
<a id="item" class="change-list__link" @click=${this.onComparePrevious} href="#">
<span class="change-list__status" title="${statusName}" aria-label="${statusName}"
>${this.renderIcon()}</span
><span class="change-list__filename">${fileName}</span>
<small class="change-list__path">${filePath}</small>
${!this.tree ? html`<small class="change-list__path">${filePath}</small>` : nothing}
</a>
<nav class="change-list__actions">
<a
@ -200,14 +203,16 @@ export class FileChangeItem extends LitElement {
title="Open file"
aria-label="Open file"
><code-icon icon="go-to-file"></code-icon></a
>${!this.uncommitted ? html`<a
class="change-list__action"
@click=${this.onCompareWorking}
href="#"
title="Open Changes with Working File"
aria-label="Open Changes with Working File"
><code-icon icon="git-compare"></code-icon></a
>` : nothing}${!this.stash && !this.uncommitted
>${!this.uncommitted
? html`<a
class="change-list__action"
@click=${this.onCompareWorking}
href="#"
title="Open Changes with Working File"
aria-label="Open Changes with Working File"
><code-icon icon="git-compare"></code-icon
></a>`
: nothing}${!this.stash && !this.uncommitted
? html`<a
class="change-list__action"
@click=${this.onOpenFileOnRemote}

+ 10
- 0
src/webviews/apps/shared/components/converters/number-converter.ts Voir le fichier

@ -0,0 +1,10 @@
import type { ValueConverter } from '@microsoft/fast-element';
export const numberConverter: ValueConverter = {
toView: function (value: number): string {
return value.toString();
},
fromView: function (value: string): number {
return parseInt(value, 10);
},
};

+ 206
- 0
src/webviews/apps/shared/components/list/file-change-list-item.ts Voir le fichier

@ -0,0 +1,206 @@
import { attr, css, customElement, FASTElement, html, ref, volatile, when } from '@microsoft/fast-element';
import type { TextDocumentShowOptions } from 'vscode';
import { numberConverter } from '../converters/number-converter';
import type { ListItem } from './list-item';
import './list-item';
import '../codicon';
export interface FileChangeListItemDetail {
path: string;
repoPath: string;
showOptions?: TextDocumentShowOptions;
}
// TODO: "change-list__action" should be a separate component
const template = html<FileChangeListItem>`
<list-item
${ref('base')}
tree="${x => x.tree}"
level="${x => x.level}"
active="${x => x.active}"
@selected="${(x, c) => x.onComparePrevious(c.event)}"
>
<img slot="icon" src="${x => x.icon}" title="${x => x.statusName}" alt="${x => x.statusName}" />
${x => x.fileName}
${when(x => !x.tree, html<FileChangeListItem>`<span slot="description">${x => x.filePath}</span>`)}
<span slot="actions">
<a
class="change-list__action"
@click="${(x, c) => x.onOpenFile(c.event)}"
href="#"
title="Open file"
aria-label="Open file"
><code-icon icon="go-to-file"></code-icon
></a>
${when(
x => !x.uncommitted,
html<FileChangeListItem>`
<a
class="change-list__action"
@click="${(x, c) => x.onCompareWorking(c.event)}"
href="#"
title="Open Changes with Working File"
aria-label="Open Changes with Working File"
><code-icon icon="git-compare"></code-icon
></a>
${when(
x => !x.stash,
html<FileChangeListItem>`<a
class="change-list__action"
@click="${(x, c) => x.onOpenFileOnRemote(c.event)}"
href="#"
title="Open on remote"
aria-label="Open on remote"
><code-icon icon="globe"></code-icon></a
><a
class="change-list__action"
@click="${(x, c) => x.onMoreActions(c.event)}"
href="#"
title="Show more actions"
aria-label="Show more actions"
><code-icon icon="ellipsis"></code-icon
></a>`,
)}
`,
)}
</span>
</list-item>
`;
const styles = css`
.change-list__action {
box-sizing: border-box;
display: inline-flex;
justify-content: center;
align-items: center;
width: 2rem;
height: 2rem;
border-radius: 0.25em;
color: inherit;
padding: 2px;
vertical-align: text-bottom;
text-decoration: none;
}
.change-list__action:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
.change-list__action:hover {
background-color: var(--vscode-toolbar-hoverBackground);
}
.change-list__action:active {
background-color: var(--vscode-toolbar-activeBackground);
}
`;
// TODO: use the model version
const statusTextMap: Record<string, string> = {
'.': 'Unchanged',
'!': 'Ignored',
'?': 'Untracked',
A: 'Added',
D: 'Deleted',
M: 'Modified',
R: 'Renamed',
C: 'Copied',
AA: 'Conflict',
AU: 'Conflict',
UA: 'Conflict',
DD: 'Conflict',
DU: 'Conflict',
UD: 'Conflict',
UU: 'Conflict',
T: 'Modified',
U: 'Updated but Unmerged',
};
@customElement({ name: 'file-change-list-item', template: template, styles: styles })
export class FileChangeListItem extends FASTElement {
base?: ListItem;
@attr({ mode: 'boolean' })
tree = false;
@attr({ converter: numberConverter })
level = 1;
@attr({ mode: 'boolean' })
active = false;
@attr({ mode: 'boolean' })
stash = false;
@attr({ mode: 'boolean' })
uncommitted = false;
@attr
icon = '';
@attr
path = '';
@attr
repo = '';
@attr
status = '';
select(showOptions?: TextDocumentShowOptions) {
this.base?.select(showOptions);
}
override focus(options?: FocusOptions | undefined): void {
this.base?.focus(options);
}
get pathIndex() {
return this.path.lastIndexOf('/');
}
@volatile
get fileName() {
return this.pathIndex > -1 ? this.path.substring(this.pathIndex + 1) : this.path;
}
@volatile
get filePath() {
return !this.tree && this.pathIndex > -1 ? this.path.substring(0, this.pathIndex) : '';
}
get statusName() {
return this.status !== '' ? statusTextMap[this.status] : '';
}
private getEventDetail(showOptions?: TextDocumentShowOptions): FileChangeListItemDetail {
return {
path: this.path,
repoPath: this.repo,
showOptions: showOptions,
};
}
onOpenFile(e: Event) {
e.preventDefault();
this.$emit('file-open', this.getEventDetail());
}
onOpenFileOnRemote(e: Event) {
e.preventDefault();
this.$emit('file-open-on-remote', this.getEventDetail());
}
onCompareWorking(e: Event) {
e.preventDefault();
this.$emit('file-compare-working', this.getEventDetail());
}
onComparePrevious(e?: Event, showOptions?: TextDocumentShowOptions) {
e?.preventDefault();
this.$emit('file-compare-previous', this.getEventDetail(showOptions));
}
onMoreActions(e: Event) {
e.preventDefault();
this.$emit('file-more-actions', this.getEventDetail());
}
}

+ 71
- 0
src/webviews/apps/shared/components/list/list-container.ts Voir le fichier

@ -0,0 +1,71 @@
import { css, customElement, FASTElement, html, observable, slotted } from '@microsoft/fast-element';
import type { FileChangeListItem } from './file-change-list-item';
import type { ListItem, ListItemSelectedDetail } from './list-item';
const template = html<ListContainer>`
<template role="tree">
<slot ${slotted('itemNodes')}></slot>
</template>
`;
const styles = css`
::slotted(*) {
box-sizing: inherit;
}
`;
type ListItemTypes = ListItem | FileChangeListItem;
@customElement({ name: 'list-container', template: template, styles: styles })
export class ListContainer extends FASTElement {
@observable
itemNodes?: ListItemTypes[];
itemNodesDisposer?: () => void;
itemNodesChanged(_oldValue?: ListItemTypes[], newValue?: ListItemTypes[]) {
this.itemNodesDisposer?.();
if (!newValue?.length) {
return;
}
const nodeEvents = newValue
?.filter(node => node.nodeType === 1)
.map(node => {
const keyHandler = this.handleKeydown.bind(this);
const selectHandler = this.handleSelected.bind(this);
node.addEventListener('keydown', keyHandler, false);
node.addEventListener('selected', selectHandler, false);
return {
dispose: function () {
node?.removeEventListener('keydown', keyHandler, false);
node?.addEventListener('selected', selectHandler, false);
},
};
});
this.itemNodesDisposer = () => {
nodeEvents?.forEach(({ dispose }) => dispose());
};
}
handleSelected(e: CustomEvent<ListItemSelectedDetail>) {
this.$emit('selected', e.detail);
}
handleKeydown(e: KeyboardEvent) {
if (!e.target) return;
const target = e.target as ListItem;
if (e.key === 'Enter' || e.key === ' ') {
target.select(e.key === 'Enter' ? { preserveFocus: false } : undefined);
} else if (e.key === 'ArrowUp') {
const $previous: HTMLElement | null = target.previousElementSibling as HTMLElement;
$previous?.focus();
} else if (e.key === 'ArrowDown') {
const $next: HTMLElement | null = target.nextElementSibling as HTMLElement;
$next?.focus();
}
}
}

+ 206
- 0
src/webviews/apps/shared/components/list/list-item.ts Voir le fichier

@ -0,0 +1,206 @@
import { attr, css, customElement, FASTElement, html, repeat, volatile, when } from '@microsoft/fast-element';
import type { TextDocumentShowOptions } from 'vscode';
import { numberConverter } from '../converters/number-converter';
import '../codicon';
declare global {
interface HTMLElementEventMap {
selected: CustomEvent;
}
}
export interface ListItemSelectedDetail {
tree: boolean;
branch: boolean;
expanded: boolean;
level: number;
}
const template = html<ListItem>`
<template role="treeitem" aria-expanded="${x => (x.branch && x.expanded === false ? 'false' : 'true')}">
<button id="item" class="item" type="button" @click="${(x, c) => x.onItemClick(c.event)}">
${repeat(
x => x.treeLeaves,
html<ListItem>`<span class="node node--connector"><code-icon name="blank"></code-icon></span>`,
)}
${when(
x => x.branch,
html<ListItem>`<span class="node"
><code-icon
class="branch"
icon="${x => (x.expanded ? 'chevron-down' : 'chevron-right')}"
></code-icon
></span>`,
)}
<span class="icon"><slot name="icon"></slot></span>
<span class="text">
<span class="main"><slot></slot></span>
<span class="description"><slot name="description"></slot></span>
</span>
</button>
<nav class="actions"><slot name="actions"></slot></nav>
</template>
`;
const styles = css`
:host {
box-sizing: border-box;
padding-left: var(--gitlens-gutter-width);
padding-right: var(--gitlens-scrollbar-gutter-width);
padding-top: 0.1rem;
padding-bottom: 0.1rem;
line-height: 2.2rem;
height: 2.2rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: var(--vscode-font-size);
color: var(--vscode-sideBar-foreground);
}
:host(:hover) {
color: var(--vscode-list-hoverForeground);
background-color: var(--vscode-list-hoverBackground);
}
:host(:focus-within) {
outline: 1px solid var(--vscode-list-focusOutline);
outline-offset: -0.1rem;
color: var(--vscode-list-activeSelectionForeground);
background-color: var(--vscode-list-activeSelectionBackground);
}
* {
box-sizing: border-box;
}
.item {
appearance: none;
display: flex;
flex-direction: row;
justify-content: flex-start;
gap: 0.6rem;
width: 100%;
padding: 0;
text-decoration: none;
color: inherit;
background: none;
border: none;
outline: none;
cursor: pointer;
}
.icon {
display: inline-block;
width: 1.6rem;
text-align: center;
}
slot[name='icon']::slotted(*) {
width: 1.6rem;
aspect-ratio: 1;
vertical-align: text-bottom;
}
.node {
display: inline-block;
width: 1.6rem;
text-align: center;
}
.node--connector {
position: relative;
}
.node--connector::before {
content: '';
position: absolute;
height: 2.2rem;
border-left: 1px solid var(--vscode-list-deemphasizedForeground);
top: 50%;
transform: translate(-50%, -50%);
left: 0.8rem;
width: 0.1rem;
opacity: 0.5;
}
.text {
overflow: hidden;
white-space: nowrap;
text-align: left;
text-overflow: ellipsis;
}
.description {
opacity: 0.7;
margin-left: 0.3rem;
}
.actions {
flex: none;
user-select: none;
color: var(--vscode-icon-foreground);
}
:host(:focus-within) .actions {
color: var(--vscode-list-activeSelectionIconForeground);
}
:host(:not(:hover):not(:focus-within)) .actions {
display: none;
}
slot[name='actions']::slotted(*) {
display: flex;
align-items: center;
}
`;
@customElement({ name: 'list-item', template: template, styles: styles })
export class ListItem extends FASTElement {
@attr({ mode: 'boolean' })
tree = false;
@attr({ mode: 'boolean' })
branch = false;
@attr({ mode: 'boolean' })
expanded = true;
@attr({ converter: numberConverter })
level = 1;
@attr({ mode: 'boolean' })
active = false;
@volatile
get treeLeaves() {
const length = this.level - 1;
if (length < 1) return [];
return Array.from({ length: length }, (_, i) => i + 1);
}
onItemClick(_e: Event) {
this.select();
}
select(_showOptions?: TextDocumentShowOptions, quiet = false) {
// TODO: this needs to be implemented
// this.expanded = !this.expanded;
this.active = true;
if (!quiet) {
this.$emit('selected', {
tree: this.tree,
branch: this.branch,
expanded: this.expanded,
level: this.level,
});
}
}
override focus(options?: FocusOptions | undefined): void {
this.shadowRoot?.getElementById('item')?.focus(options);
}
}

+ 19
- 6
src/webviews/commitDetails/commitDetailsWebviewView.ts Voir le fichier

@ -77,6 +77,7 @@ export class CommitDetailsWebviewView extends WebviewViewBase
commit: undefined,
preferences: {
autolinksExpanded: this.container.storage.getWorkspace('views:commitDetails:autolinksExpanded'),
filesAsTree: this.container.storage.getWorkspace('views:commitDetails:filesAsTree'),
},
richStateLoaded: false,
formattedMessage: undefined,
@ -430,14 +431,26 @@ export class CommitDetailsWebviewView extends WebviewViewBase
}
private updatePreferences(preferences: SavedPreferences) {
if (this._context.preferences?.autolinksExpanded === preferences.autolinksExpanded) return;
if (this._context.preferences?.autolinksExpanded === preferences.autolinksExpanded && this._context.preferences?.filesAsTree === preferences.filesAsTree) return;
void this.container.storage.storeWorkspace(
'views:commitDetails:autolinksExpanded',
preferences.autolinksExpanded,
);
const changes: SavedPreferences = {};
if (this._context.preferences?.autolinksExpanded !== preferences.autolinksExpanded) {
void this.container.storage.storeWorkspace(
'views:commitDetails:autolinksExpanded',
preferences.autolinksExpanded,
);
changes.autolinksExpanded = preferences.autolinksExpanded;
}
if (this._context.preferences?.filesAsTree !== preferences.filesAsTree) {
void this.container.storage.storeWorkspace(
'views:commitDetails:filesAsTree',
preferences.filesAsTree,
);
changes.filesAsTree = preferences.filesAsTree;
}
this.updatePendingContext({ preferences: { autolinksExpanded: preferences.autolinksExpanded } });
this.updatePendingContext({ preferences: changes });
}
private updatePendingContext(context: Partial<Context>, force: boolean = false): boolean {

+ 3
- 1
src/webviews/commitDetails/protocol.ts Voir le fichier

@ -25,6 +25,7 @@ export type CommitDetails = CommitSummary & {
export type SavedPreferences = {
autolinksExpanded?: boolean;
filesAsTree?: boolean;
};
export type State = {
@ -72,7 +73,8 @@ export interface PinParams {
export const PinCommitCommandType = new IpcCommandType<PinParams>('commit/pin');
export interface PreferenceParams {
autolinksExpanded: boolean;
autolinksExpanded?: boolean;
filesAsTree?: boolean;
}
export const PreferencesCommandType = new IpcCommandType<PreferenceParams>('commit/preferences');

Chargement…
Annuler
Enregistrer