Browse Source

Updates the commit graph header

- moves repo to the top
- adds branch and branch switching
- adds fetching
- moves account status and feedback to header
- adds PopMenu and MenuList components
main
Keith Daulton 2 years ago
parent
commit
3cbc8a1266
14 changed files with 637 additions and 324 deletions
  1. +7
    -0
      CHANGELOG.md
  2. +51
    -7
      src/plus/webviews/graph/graphWebview.ts
  3. +7
    -0
      src/plus/webviews/graph/protocol.ts
  4. +117
    -96
      src/webviews/apps/plus/graph/GraphWrapper.tsx
  5. +219
    -221
      src/webviews/apps/plus/graph/graph.scss
  6. +8
    -0
      src/webviews/apps/plus/graph/graph.tsx
  7. +3
    -0
      src/webviews/apps/shared/components/menu/index.ts
  8. +49
    -0
      src/webviews/apps/shared/components/menu/menu-item.ts
  9. +24
    -0
      src/webviews/apps/shared/components/menu/menu-label.ts
  10. +21
    -0
      src/webviews/apps/shared/components/menu/menu-list.ts
  11. +6
    -0
      src/webviews/apps/shared/components/menu/react.tsx
  12. +1
    -0
      src/webviews/apps/shared/components/overlays/pop-menu/index.ts
  13. +120
    -0
      src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts
  14. +4
    -0
      src/webviews/apps/shared/components/overlays/pop-menu/react.tsx

+ 7
- 0
CHANGELOG.md View File

@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [Unreleased]
### Added
- Adds new features to the _Commit Graph_ header
- Adds the current branch name with the ability to switch branches when clicked
- Adds a fetch action next to the branch name
- Moves the repo menu, account status and feedback links to the top
### Fixed
- Fixes [#2377](https://github.com/gitkraken/vscode-gitlens/issues/2377) - Missing Azure Devops Icon

+ 51
- 7
src/plus/webviews/graph/graphWebview.ts View File

@ -47,12 +47,8 @@ import type {
} from '../../../git/models/reference';
import { GitReference, GitRevision } from '../../../git/models/reference';
import { getRemoteIconUri } from '../../../git/models/remote';
import type {
Repository,
RepositoryChangeEvent,
RepositoryFileSystemChangeEvent,
} from '../../../git/models/repository';
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository';
import type { RepositoryChangeEvent, RepositoryFileSystemChangeEvent } from '../../../git/models/repository';
import { Repository, RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository';
import type { GitSearch } from '../../../git/search';
import { getSearchQueryComparisonKey } from '../../../git/search';
import type { StoredGraphHiddenRef } from '../../../storage';
@ -60,7 +56,7 @@ import { executeActionCommand, executeCommand, executeCoreGitCommand, registerCo
import { gate } from '../../../system/decorators/gate';
import { debug } from '../../../system/decorators/log';
import type { Deferrable } from '../../../system/function';
import { debounce, once } from '../../../system/function';
import { debounce, disposableInterval, once } from '../../../system/function';
import { last } from '../../../system/iterable';
import { updateRecordValue } from '../../../system/object';
import { getSettledValue } from '../../../system/promise';
@ -119,6 +115,7 @@ import {
DidChangeSubscriptionNotificationType,
DidChangeWorkingTreeNotificationType,
DidEnsureRowNotificationType,
DidFetchNotificationType,
DidSearchNotificationType,
DismissBannerCommandType,
DoubleClickedRefCommandType,
@ -201,6 +198,7 @@ export class GraphWebview extends WebviewBase {
private _statusBarItem: StatusBarItem | undefined;
private _theme: ColorTheme | undefined;
private _repositoryEventsDisposable: Disposable | undefined;
private _lastFetchedDisposable: Disposable | undefined;
private trialBanner?: boolean;
@ -1028,6 +1026,19 @@ export class GraphWebview extends WebviewBase {
}
@debug()
private async notifyDidFetch() {
if (!this.isReady || !this.visible) {
this.addPendingIpcNotification(DidFetchNotificationType);
return false;
}
const lastFetched = await this.repository!.getLastFetched();
return this.notify(DidFetchNotificationType, {
lastFetched: new Date(lastFetched),
});
}
@debug()
private async notifyDidChangeRows(sendSelectedRows: boolean = false, completionId?: string) {
if (this._graph == null) return;
@ -1161,6 +1172,7 @@ export class GraphWebview extends WebviewBase {
}
private ensureRepositorySubscriptions(force?: boolean) {
void this.ensureLastFetchedSubscription(force);
if (!force && this._repositoryEventsDisposable != null) return;
if (this._repositoryEventsDisposable != null) {
@ -1184,6 +1196,33 @@ export class GraphWebview extends WebviewBase {
);
}
private async ensureLastFetchedSubscription(force?: boolean) {
if (!force && this._lastFetchedDisposable != null) return;
if (this._lastFetchedDisposable != null) {
this._lastFetchedDisposable.dispose();
this._lastFetchedDisposable = undefined;
}
const repo = this.repository;
if (repo == null) return;
const lastFetched = (await repo.getLastFetched()) ?? 0;
let interval = Repository.getLastFetchedUpdateInterval(lastFetched);
if (lastFetched !== 0 && interval > 0) {
this._lastFetchedDisposable = disposableInterval(() => {
// Check if the interval should change, and if so, reset it
const checkInterval = Repository.getLastFetchedUpdateInterval(lastFetched);
if (interval !== Repository.getLastFetchedUpdateInterval(lastFetched)) {
interval = checkInterval;
}
void this.notifyDidFetch();
}, interval);
}
}
private async ensureSearchStartsInRange(graph: GitGraph, search: GitSearch) {
if (search.results.size === 0) return undefined;
@ -1407,11 +1446,16 @@ export class GraphWebview extends WebviewBase {
const columns = this.getColumns();
const lastFetched = await this.repository.getLastFetched();
const branch = await this.repository.getBranch();
return {
trialBanner: this.trialBanner,
repositories: formatRepositories(this.container.git.openRepositories),
selectedRepository: this.repository.path,
selectedRepositoryVisibility: visibility,
branchName: branch?.name,
lastFetched: new Date(lastFetched),
selectedRows: this._selectedRows,
subscription: access?.subscription.current,
allowed: (access?.allowed ?? false) !== false,

+ 7
- 0
src/plus/webviews/graph/protocol.ts View File

@ -41,6 +41,8 @@ export interface State {
repositories?: GraphRepository[];
selectedRepository?: string;
selectedRepositoryVisibility?: RepositoryVisibility;
branchName?: string;
lastFetched?: Date;
selectedRows?: GraphSelectedRows;
subscription?: Subscription;
allowed: boolean;
@ -299,3 +301,8 @@ export interface DidSearchParams {
selectedRows?: GraphSelectedRows;
}
export const DidSearchNotificationType = new IpcNotificationType<DidSearchParams>('graph/didSearch', true);
export interface DidFetchParams {
lastFetched: Date;
}
export const DidFetchNotificationType = new IpcNotificationType<DidFetchParams>('graph/didFetch');

+ 117
- 96
src/webviews/apps/plus/graph/GraphWrapper.tsx View File

@ -43,12 +43,15 @@ import {
DidChangeSelectionNotificationType,
DidChangeSubscriptionNotificationType,
DidChangeWorkingTreeNotificationType,
DidFetchNotificationType,
DidSearchNotificationType,
} from '../../../../plus/webviews/graph/protocol';
import type { Subscription } from '../../../../subscription';
import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../subscription';
import { pluralize } from '../../../../system/string';
import type { IpcNotificationType } from '../../../../webviews/protocol';
import { MenuItem, MenuList } from '../../shared/components/menu/react';
import { PopMenu } from '../../shared/components/overlays/pop-menu/react';
import { PopOver } from '../../shared/components/overlays/react';
import { SearchBox } from '../../shared/components/search/react';
import type { SearchNavigationEventDetail } from '../../shared/components/search/search-box';
@ -164,6 +167,8 @@ export function GraphWrapper({
const [pagingHasMore, setPagingHasMore] = useState(state.paging?.hasMore ?? false);
const [isLoading, setIsLoading] = useState(state.loading);
const [styleProps, setStyleProps] = useState(state.theming);
const [branchName, setBranchName] = useState(state.branchName);
const [lastFetched, setLastFetched] = useState(state.lastFetched);
// account
const [showAccount, setShowAccount] = useState(state.trialBanner);
const [isAccessAllowed, setIsAccessAllowed] = useState(state.allowed ?? false);
@ -171,8 +176,6 @@ export function GraphWrapper({
state.selectedRepositoryVisibility === RepositoryVisibility.Private,
);
const [subscription, setSubscription] = useState<Subscription | undefined>(state.subscription);
// repo selection UI
const [repoExpanded, setRepoExpanded] = useState(false);
// search state
const searchEl = useRef<any>(null);
const [searchQuery, setSearchQuery] = useState<SearchQuery | undefined>(undefined);
@ -247,11 +250,16 @@ export function GraphWrapper({
case DidChangeWorkingTreeNotificationType:
setWorkingTreeStats(state.workingTreeStats ?? { added: 0, modified: 0, deleted: 0 });
break;
case DidFetchNotificationType:
setLastFetched(state.lastFetched);
break;
default: {
setIsAccessAllowed(state.allowed ?? false);
if (!themingChanged) {
setStyleProps(state.theming);
}
setBranchName(state.branchName);
setLastFetched(state.lastFetched);
setColumns(state.columns);
setRows(state.rows ?? []);
setWorkingTreeStats(state.workingTreeStats ?? { added: 0, modified: 0, deleted: 0 });
@ -440,12 +448,6 @@ export function GraphWrapper({
setIsLoading(true);
onSelectRepository?.(item);
}
setRepoExpanded(false);
};
const handleToggleRepos = () => {
if (repo != null && repos.length <= 1) return;
setRepoExpanded(!repoExpanded);
};
const handleMissingAvatars = (emails: GraphAvatars) => {
@ -556,7 +558,7 @@ export function GraphWrapper({
</>
)}
</span>
<PopOver placement="bottom end" className="badge-popover">
<PopOver placement="top end" className="badge-popover">
{isPro
? 'You have access to all GitLens and GitLens+ features on any repo.'
: 'You have access to GitLens+ features on local & public repos, and all other GitLens features on any repo.'}
@ -699,22 +701,112 @@ export function GraphWrapper({
{renderAlertContent()}
{isAccessAllowed && (
<header className="titlebar graph-app__header">
<div className="titlebar__group">
<SearchBox
ref={searchEl}
step={searchPosition}
total={searchResults?.count ?? 0}
valid={Boolean(searchQuery?.query && searchQuery.query.length > 2)}
more={searchResults?.paging?.hasMore ?? false}
searching={searching}
value={searchQuery?.query ?? ''}
errorMessage={searchResultsError?.error ?? ''}
resultsHidden={searchResultsHidden}
resultsLoaded={searchResults != null}
onChange={e => handleSearchInput(e as CustomEvent<SearchQuery>)}
onNavigate={e => handleSearchNavigation(e as CustomEvent<SearchNavigationEventDetail>)}
onOpenInView={() => handleSearchOpenInView()}
/>
<div className="titlebar__row">
<div className="titlebar__group">
{repos.length < 2 ? (
<button type="button" className="action-button" disabled>
{repo?.formattedName ?? 'none selected'}
</button>
) : (
<PopMenu>
<button
type="button"
className="action-button"
slot="trigger"
disabled={repos.length < 2}
>
{repo?.formattedName ?? 'none selected'}
{repos.length > 1 && (
<span
className="codicon codicon-chevron-down action-button__more"
aria-hidden="true"
></span>
)}
</button>
<MenuList role="listbox" slot="content">
{repos.length > 0 &&
repos.map((item, index) => (
<MenuItem
aria-selected={item.path === repo?.path}
onClick={() => handleSelectRepository(item)}
disabled={item.path === repo?.path}
key={`repo-actioncombo-item-${index}`}
>
<span
className={`${
item.path === repo?.path ? 'codicon codicon-check ' : ''
}actioncombo__icon`}
aria-label="Checked"
></span>
{item.formattedName}
</MenuItem>
))}
</MenuList>
</PopMenu>
)}
{repo && (
<>
<span>
<span className="codicon codicon-chevron-right"></span>
</span>
<a href="command:gitlens.graph.switchToAnotherBranch" className="action-button">
{branchName}
<span
className="codicon codicon-chevron-down action-button__more"
aria-hidden="true"
></span>
</a>
<span>
<span className="codicon codicon-chevron-right"></span>
</span>
<a href="command:gitlens.graph.fetch" className="action-button">
<span className="codicon codicon-sync action-button__icon"></span> Fetch{' '}
{lastFetched && <small>(Last fetched {fromNow(new Date(lastFetched))})</small>}
</a>
</>
)}
</div>
<div className="titlebar__group titlebar__group--fixed">
{renderAccountState()}
<a
href="https://github.com/gitkraken/vscode-gitlens/discussions/2158"
title="Commit Graph Feedback"
aria-label="Commit Graph Feedback"
className="action-button"
>
<span className="codicon codicon-feedback"></span>
</a>
</div>
</div>
<div className="titlebar__row">
<div className="titlebar__group">
{/* <span className="action-button">
<span className="codicon codicon-filter"></span>
<span className="action-button__indicator"></span>
<span
className="codicon codicon-chevron-down action-button__more"
aria-hidden="true"
></span>
</span>
<span>
<span className="action-divider"></span>
</span> */}
<SearchBox
ref={searchEl}
step={searchPosition}
total={searchResults?.count ?? 0}
valid={Boolean(searchQuery?.query && searchQuery.query.length > 2)}
more={searchResults?.paging?.hasMore ?? false}
searching={searching}
value={searchQuery?.query ?? ''}
errorMessage={searchResultsError?.error ?? ''}
resultsHidden={searchResultsHidden}
resultsLoaded={searchResults != null}
onChange={e => handleSearchInput(e as CustomEvent<SearchQuery>)}
onNavigate={e => handleSearchNavigation(e as CustomEvent<SearchNavigationEventDetail>)}
onOpenInView={() => handleSearchOpenInView()}
/>
</div>
</div>
</header>
)}
@ -784,67 +876,6 @@ export function GraphWrapper({
aria-hidden={!isAccessAllowed}
>
<div className="actionbar__group">
<div className="actioncombo">
<button
type="button"
aria-controls="repo-actioncombo-list"
aria-expanded={repoExpanded}
aria-haspopup="listbox"
id="repo-actioncombo-label"
className="actioncombo__label"
disabled={repos.length < 2}
role="combobox"
aria-activedescendant={
repoExpanded
? `repo-actioncombo-item-${repos.findIndex(item => item.path === repo?.path)}`
: undefined
}
onClick={() => handleToggleRepos()}
>
<span className="codicon codicon-repo actioncombo__icon" aria-label="Repository "></span>
{repo?.formattedName ?? 'none selected'}
</button>
<div
className="actioncombo__list"
id="repo-actioncombo-list"
role="listbox"
tabIndex={-1}
aria-labelledby="repo-actioncombo-label"
>
{repos.length > 0 ? (
repos.map((item, index) => (
<button
type="button"
className="actioncombo__item"
role="option"
data-value={item.path}
id={`repo-actioncombo-item-${index}`}
key={`repo-actioncombo-item-${index}`}
aria-selected={item.path === repo?.path}
onClick={() => handleSelectRepository(item)}
disabled={item.path === repo?.path}
>
<span
className={`${
item.path === repo?.path ? 'codicon codicon-check ' : ''
}actioncombo__icon`}
aria-label="Checked"
></span>
{item.formattedName}
</button>
))
) : (
<span
className="actioncombo__item"
role="option"
id="repo-actioncombo-item-0"
aria-selected="true"
>
None available
</span>
)}
</div>
</div>
{isAccessAllowed && rows.length > 0 && (
<span className="actionbar__details">
showing {rows.length} item{rows.length ? 's' : ''}
@ -856,16 +887,6 @@ export function GraphWrapper({
</span>
)}
</div>
<div className="actionbar__group">
{renderAccountState()}
<a
href="https://github.com/gitkraken/vscode-gitlens/discussions/2158"
title="Commit Graph Feedback"
aria-label="Commit Graph Feedback"
>
<span className="codicon codicon-feedback"></span>
</a>
</div>
<div className={`progress-container infinite${isLoading ? ' active' : ''}`} role="progressbar">
<div className="progress-bar"></div>
</div>

+ 219
- 221
src/webviews/apps/plus/graph/graph.scss View File

@ -3,6 +3,11 @@
@import '../../shared/glicons';
@import '../../../../../node_modules/@gitkraken/gitkraken-components/dist/styles.css';
@mixin focusStyles() {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
.vscode-high-contrast,
.vscode-dark {
--popover-bg: var(--color-background--lighten-15);
@ -57,9 +62,19 @@ a {
}
}
a,
button:not([disabled]),
[tabindex]:not([tabindex='-1']) {
&:focus {
@include focusStyles();
}
}
.badge {
font-size: 1rem;
font-weight: 700;
text-transform: uppercase;
color: var(--color-foreground);
&.is-help {
cursor: help;
@ -78,7 +93,7 @@ a {
&-popover {
width: max-content;
right: 0;
bottom: 100%;
top: 100%;
}
&:not(:hover) + &-popover {
@ -109,6 +124,7 @@ a {
}
&__details {
line-height: var(--actionbar-height);
}
&__loading {
@ -127,111 +143,83 @@ a {
top: -2px;
}
}
:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
}
.actioncombo {
$block: &;
$block-expanded: #{$block}--expanded;
--actioncombo-height: 2.2rem;
--actioncombo-items: 1;
--actioncombo-items-height: 2.4rem;
.action-button {
position: relative;
display: inline-flex;
flex-direction: row;
align-items: stretch;
appearance: none;
font-family: inherit;
font-size: 1.2rem;
gap: 0.25rem;
height: var(--actioncombo-height);
line-height: var(--actioncombo-height);
&__label {
appearance: none;
font-family: inherit;
background-color: var(--color-graph-actionbar-background);
border: none;
color: inherit;
padding: 0 0.75rem;
cursor: pointer;
height: var(--actioncombo-height);
line-height: var(--actioncombo-height);
border-radius: 3px;
&[disabled] {
pointer-events: none;
cursor: default;
opacity: 1;
}
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;
&:hover {
background-color: var(--color-graph-actionbar-selectedBackground);
}
&[disabled] {
pointer-events: none;
cursor: default;
opacity: 1;
}
&__list {
position: absolute;
left: 0;
bottom: 100%;
display: flex;
flex-direction: column;
justify-content: stretch;
min-width: 100%;
width: max-content;
z-index: 10000;
background-color: var(--vscode-menu-background);
border: 1px solid var(--vscode-menu-border);
&:hover {
background-color: var(--color-graph-actionbar-selectedBackground);
color: var(--color-foreground);
text-decoration: none;
}
&__label:not([aria-expanded='true']) + &__list {
display: none;
.codicon[class*='codicon-'] {
line-height: 2.2rem;
vertical-align: bottom;
}
&__item {
appearance: none;
font-family: inherit;
border: none;
padding: 0 0.6rem;
cursor: pointer;
color: var(--vscode-menu-foreground);
background-color: var(--vscode-menu-background);
text-align: left;
display: flex;
align-items: center;
height: var(--actioncombo-items-height);
line-height: var(--actioncombo-items-height);
&:hover {
color: var(--vscode-menu-selectionForeground);
background-color: var(--vscode-menu-selectionBackground);
}
&__icon,
&__icon.codicon[class*='codicon-'] {
margin-right: 0.4rem;
}
&__icon:not(.codicon) {
display: inline-block;
width: 1.6rem;
}
&[disabled] {
pointer-events: none;
cursor: default;
opacity: 0.5;
}
&__more,
&__more.codicon[class*='codicon-'] {
font-size: 1rem;
margin-left: 0.4rem;
margin-right: -0.35rem;
}
&[aria-selected='true'] {
opacity: 1;
color: var(--vscode-menu-selectionForeground);
background-color: var(--vscode-menu-background);
}
&__indicator {
position: absolute;
bottom: 0.2rem;
right: 1.5rem;
display: block;
width: 0.8rem;
height: 0.8rem;
border-radius: 100%;
background-color: var(--vscode-focusBorder);
}
&__icon.codicon[class*='codicon-'] {
margin-right: 0.4rem;
small {
opacity: 0.6;
}
}
&__icon:not(.codicon) {
display: inline-block;
margin-right: 0.4rem;
width: 16px;
.action-divider {
display: inline-block;
width: 0.1rem;
height: 2.2rem;
vertical-align: middle;
background-color: var(--vscode-titleBar-inactiveForeground);
opacity: 0.4;
margin: {
// left: 0.2rem;
right: 0.2rem;
}
}
@ -271,8 +259,7 @@ a {
}
&:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
@include focusStyles;
}
&[disabled] {
@ -431,128 +418,126 @@ a {
letter-spacing: normal;
}
.icon--head {
&::before {
// codicon-vm
font-family: codicon;
content: '\ea7a';
.icon {
&--head {
&::before {
// codicon-vm
font-family: codicon;
content: '\ea7a';
}
}
}
.icon--remote {
&::before {
// codicon-cloud
font-family: codicon;
content: '\ebaa';
&--remote {
&::before {
// codicon-cloud
font-family: codicon;
content: '\ebaa';
}
}
}
.icon--tag {
&::before {
// codicon-tag
font-family: codicon;
content: '\ea66';
&--tag {
&::before {
// codicon-tag
font-family: codicon;
content: '\ea66';
}
}
}
.icon--stash {
&::before {
// codicon-inbox
font-family: codicon;
content: '\eb09';
&--stash {
&::before {
// codicon-inbox
font-family: codicon;
content: '\eb09';
}
}
}
.icon--check {
&::before {
// codicon-check
font-family: codicon;
content: '\eab2';
&--check {
&::before {
// codicon-check
font-family: codicon;
content: '\eab2';
}
}
}
.icon--warning {
:before {
// codicon-vm
font-family: codicon;
content: '\ea6c';
&--warning {
:before {
// codicon-vm
font-family: codicon;
content: '\ea6c';
}
color: #de9b43;
}
color: #de9b43;
}
.icon--added {
&::before {
// codicon-add
font-family: codicon;
content: '\ea60';
&--added {
&::before {
// codicon-add
font-family: codicon;
content: '\ea60';
}
}
}
.icon--modified {
&::before {
// codicon-edit
font-family: codicon;
content: '\ea73';
&--modified {
&::before {
// codicon-edit
font-family: codicon;
content: '\ea73';
}
}
}
.icon--deleted {
&::before {
// codicon-dash
font-family: codicon;
content: '\eacc';
&--deleted {
&::before {
// codicon-dash
font-family: codicon;
content: '\eacc';
}
}
}
.icon--renamed {
&::before {
// codicon-file
font-family: codicon;
content: '\eb60';
&--renamed {
&::before {
// codicon-file
font-family: codicon;
content: '\eb60';
}
}
}
.icon--resolved {
&::before {
// codicon-pass-filled
font-family: codicon;
content: '\ebb3';
&--resolved {
&::before {
// codicon-pass-filled
font-family: codicon;
content: '\ebb3';
}
}
}
.icon--hide {
&::before {
// codicon-eye-closed
font-family: codicon;
content: '\eae7';
&--hide {
&::before {
// codicon-eye-closed
font-family: codicon;
content: '\eae7';
}
}
}
.icon--show {
&::before {
// codicon-eye
font-family: codicon;
content: '\ea70';
&--show {
&::before {
// codicon-eye
font-family: codicon;
content: '\ea70';
}
}
}
.icon--pull-request {
&::before {
// codicon-git-pull-request
font-family: codicon;
content: '\ea64';
&--pull-request {
&::before {
// codicon-git-pull-request
font-family: codicon;
content: '\ea64';
}
}
}
.titlebar {
background: var(--vscode-titleBar-inactiveBackground);
color: var(--vscode-titleBar-inactiveForeground);
height: 3.6rem;
// height: 3.6rem;
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;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
@ -561,8 +546,21 @@ a {
}
}
&,
&__row {
justify-content: space-between;
}
&__row {
flex: 0 0 100%;
}
&__group {
flex: auto 1 1;
&--fixed {
flex: none;
}
}
}
@ -678,8 +676,7 @@ a {
}
&:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
@include focusStyles;
}
&.active,
@ -797,42 +794,43 @@ a {
}
}
.tooltip.in {
opacity: 1;
}
.tooltip.top .tooltip-arrow {
border-top-color: var(--color-hover-border);
}
.tooltip.top-left .tooltip-arrow {
border-top-color: var(--color-hover-border);
}
.tooltip.top-right .tooltip-arrow {
border-top-color: var(--color-hover-border);
}
.tooltip.right .tooltip-arrow {
border-right-color: var(--color-hover-border);
}
.tooltip.left .tooltip-arrow {
border-left-color: var(--color-hover-border);
}
.tooltip.bottom .tooltip-arrow {
border-bottom-color: var(--color-hover-border);
}
.tooltip.bottom-left .tooltip-arrow {
border-bottom-color: var(--color-hover-border);
}
.tooltip.bottom-right .tooltip-arrow {
border-bottom-color: var(--color-hover-border);
}
.tooltip {
font-size: var(--vscode-font-size);
font-family: var(--vscode-font-family);
}
.tooltip-inner {
font-size: 1.2rem;
padding: 0.3rem 0.6rem;
color: var(--color-hover-foreground);
background-color: var(--color-hover-background);
border: 0.1rem solid var(--color-hover-border);
border-radius: 0;
&.in {
opacity: 1;
}
&.top .tooltip-arrow {
border-top-color: var(--color-hover-border);
}
&.top-left .tooltip-arrow {
border-top-color: var(--color-hover-border);
}
&.top-right .tooltip-arrow {
border-top-color: var(--color-hover-border);
}
&.right .tooltip-arrow {
border-right-color: var(--color-hover-border);
}
&.left .tooltip-arrow {
border-left-color: var(--color-hover-border);
}
&.bottom .tooltip-arrow {
border-bottom-color: var(--color-hover-border);
}
&.bottom-left .tooltip-arrow {
border-bottom-color: var(--color-hover-border);
}
&.bottom-right .tooltip-arrow {
border-bottom-color: var(--color-hover-border);
}
&-inner {
font-size: 1.2rem;
padding: 0.3rem 0.6rem;
color: var(--color-hover-foreground);
background-color: var(--color-hover-background);
border: 0.1rem solid var(--color-hover-border);
border-radius: 0;
}
}

+ 8
- 0
src/webviews/apps/plus/graph/graph.tsx View File

@ -27,6 +27,7 @@ import {
DidChangeSubscriptionNotificationType,
DidChangeWorkingTreeNotificationType,
DidEnsureRowNotificationType,
DidFetchNotificationType,
DidSearchNotificationType,
DismissBannerCommandType,
DoubleClickedRefCommandType,
@ -129,6 +130,13 @@ export class GraphApp extends App {
});
break;
case DidFetchNotificationType.method:
onIpc(DidFetchNotificationType, msg, (params, type) => {
this.state.lastFetched = params.lastFetched;
this.setState(this.state, type);
});
break;
case DidChangeAvatarsNotificationType.method:
onIpc(DidChangeAvatarsNotificationType, msg, (params, type) => {
this.state.avatars = params.avatars;

+ 3
- 0
src/webviews/apps/shared/components/menu/index.ts View File

@ -0,0 +1,3 @@
export * from './menu-list';
export * from './menu-item';
export * from './menu-label';

+ 49
- 0
src/webviews/apps/shared/components/menu/menu-item.ts View File

@ -0,0 +1,49 @@
import { attr, css, customElement, FASTElement, html } from '@microsoft/fast-element';
import { elementBase } from '../styles/base';
const template = html<MenuItem>`
<template role="option" tabindex="${x => (x.disabled ? '-1' : '0')}" ?disabled="${x => x.disabled}">
<slot></slot>
</template>
`;
const styles = css`
${elementBase}
:host {
font-family: inherit;
border: none;
padding: 0 0.6rem;
cursor: pointer;
color: var(--vscode-menu-foreground);
background-color: var(--vscode-menu-background);
text-align: left;
display: flex;
align-items: center;
height: auto;
line-height: 2.2rem;
}
:host(:hover) {
color: var(--vscode-menu-selectionForeground);
background-color: var(--vscode-menu-selectionBackground);
}
:host([disabled]) {
pointer-events: none;
cursor: default;
opacity: 0.5;
}
:host([aria-selected='true']) {
opacity: 1;
color: var(--vscode-menu-selectionForeground);
background-color: var(--vscode-menu-background);
}
`;
@customElement({ name: 'menu-item', template: template, styles: styles })
export class MenuItem extends FASTElement {
@attr({ mode: 'boolean' })
disabled = false;
}

+ 24
- 0
src/webviews/apps/shared/components/menu/menu-label.ts View File

@ -0,0 +1,24 @@
import { css, customElement, FASTElement, html } from '@microsoft/fast-element';
import { elementBase } from '../styles/base';
const template = html<MenuLabel>`
<template>
<slot></slot>
</template>
`;
const styles = css`
${elementBase}
:host {
text-transform: uppercase;
font-size: 0.84em;
line-height: 2.2rem;
padding-left: 0.6rem;
padding-right: 0.6rem;
margin: 0px;
}
`;
@customElement({ name: 'menu-label', template: template, styles: styles })
export class MenuLabel extends FASTElement {}

+ 21
- 0
src/webviews/apps/shared/components/menu/menu-list.ts View File

@ -0,0 +1,21 @@
import { css, customElement, FASTElement, html } from '@microsoft/fast-element';
import { elementBase } from '../styles/base';
const template = html<MenuList>`
<template role="listbox">
<slot></slot>
</template>
`;
const styles = css`
${elementBase}
:host {
width: max-content;
background-color: var(--vscode-menu-background);
border: 1px solid var(--vscode-menu-border);
}
`;
@customElement({ name: 'menu-list', template: template, styles: styles })
export class MenuList extends FASTElement {}

+ 6
- 0
src/webviews/apps/shared/components/menu/react.tsx View File

@ -0,0 +1,6 @@
import { reactWrapper } from '../helpers/react-wrapper';
import { MenuItem as MenuItemComponent, MenuLabel as MenuLabelComponent, MenuList as MenuListComponent } from './index';
export const MenuItem = reactWrapper(MenuItemComponent);
export const MenuLabel = reactWrapper(MenuLabelComponent);
export const MenuList = reactWrapper(MenuListComponent);

+ 1
- 0
src/webviews/apps/shared/components/overlays/pop-menu/index.ts View File

@ -0,0 +1 @@
export * from './pop-menu';

+ 120
- 0
src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts View File

@ -0,0 +1,120 @@
import { attr, css, customElement, FASTElement, html, observable, slotted, volatile } from '@microsoft/fast-element';
import { hasNodes, nodeTypeFilter } from '../../helpers/slots';
import { elementBase } from '../../styles/base';
const template = html<PopMenu>`
<template role="combobox">
<slot ${slotted({ property: 'triggerNodes', filter: nodeTypeFilter(Node.ELEMENT_NODE) })} name="trigger"></slot>
<slot ${slotted({ property: 'contentNodes', filter: nodeTypeFilter(Node.ELEMENT_NODE) })} name="content"></slot>
</template>
`;
const styles = css`
${elementBase}
:host {
position: relative;
}
slot[name='content']::slotted(*) {
position: absolute;
left: 0;
top: 100%;
z-index: 10000;
}
:host(:not([open])) slot[name='content']::slotted(*) {
display: none;
}
`;
@customElement({ name: 'pop-menu', template: template, styles: styles })
export class PopMenu extends FASTElement {
@attr({ mode: 'boolean' })
open = false;
@observable
triggerNodes?: HTMLElement[];
@observable
contentNodes?: HTMLElement[];
@volatile
get triggerNode() {
if (!hasNodes(this.triggerNodes)) {
return;
}
return this.triggerNodes![0];
}
@volatile
get contentNode() {
if (!hasNodes(this.contentNodes)) {
return;
}
return this.contentNodes![0];
}
isTrackingOutside = false;
override connectedCallback() {
super.connectedCallback();
this.updateToggle();
this.addEventListener('click', this.handleToggle.bind(this), false);
}
override disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('click', this.handleToggle.bind(this), false);
this.disposeTrackOutside();
}
handleToggle() {
this.open = !this.open;
this.updateToggle();
}
updateToggle() {
if (this.triggerNode != null) {
this.triggerNode.ariaExpanded = this.open.toString();
}
if (this.open) {
if (this.contentNode != null) {
window.requestAnimationFrame(() => {
this.contentNode?.focus();
});
}
this.trackOutside();
}
}
handleDocumentEvent(e: MouseEvent | FocusEvent) {
if (this.open === false) return;
const composedPath = e.composedPath();
if (!composedPath.includes(this)) {
this.open = false;
this.disposeTrackOutside();
}
}
trackOutside() {
if (this.isTrackingOutside || !this.open) return;
this.isTrackingOutside = true;
const boundHandleDocumentEvent = this.handleDocumentEvent.bind(this);
document.addEventListener('click', boundHandleDocumentEvent, false);
document.addEventListener('focusin', boundHandleDocumentEvent, false);
}
disposeTrackOutside() {
if (!this.isTrackingOutside) return;
this.isTrackingOutside = false;
const boundHandleDocumentEvent = this.handleDocumentEvent.bind(this);
document.removeEventListener('click', boundHandleDocumentEvent, false);
window.removeEventListener('focusin', boundHandleDocumentEvent, false);
}
}

+ 4
- 0
src/webviews/apps/shared/components/overlays/pop-menu/react.tsx View File

@ -0,0 +1,4 @@
import { reactWrapper } from '../../helpers/react-wrapper';
import { PopMenu as PopMenuComponent } from './index';
export const PopMenu = reactWrapper(PopMenuComponent);

Loading…
Cancel
Save