Skip to content

Commit

Permalink
Adds search controls to the graph
Browse files Browse the repository at this point in the history
  • Loading branch information
d13 authored and eamodio committed Sep 23, 2022
1 parent 7dbc10a commit d2bd3da
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 8 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -11723,7 +11723,8 @@
},
"dependencies": {
"@gitkraken/gitkraken-components": "1.0.0-rc.15",
"@microsoft/fast-element": "^1.10.5",
"@microsoft/fast-element": "1.10.5",
"@microsoft/fast-react-wrapper": "0.3.14",
"@octokit/core": "4.0.5",
"@vscode/codicons": "0.0.32",
"@vscode/webview-ui-toolkit": "1.1.0",
Expand Down
82 changes: 80 additions & 2 deletions src/webviews/apps/plus/graph/GraphWrapper.tsx
Expand Up @@ -7,8 +7,8 @@ import GraphContainer, {
type GraphRow,
type GraphZoneType,
} from '@gitkraken/gitkraken-components';
import type { ReactElement } from 'react';
import React, { createElement, useEffect, useRef, useState } from 'react';
import type { FormEvent, ReactElement } from 'react';
import React, { createElement, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { getPlatform } from '@env/platform';
import { DateStyle } from '../../../../config';
import type { GraphColumnConfig } from '../../../../config';
Expand All @@ -24,6 +24,7 @@ import type {
import type { Subscription } from '../../../../subscription';
import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../subscription';
import { pluralize } from '../../../../system/string';
import { SearchNav } from '../../shared/components/search/search-nav-react';
import type { DateTimeFormat } from '../../shared/date';
import { formatDate, fromNow } from '../../shared/date';

Expand Down Expand Up @@ -223,8 +224,61 @@ export function GraphWrapper({
const [repoExpanded, setRepoExpanded] = useState(false);
// column setting UI
const [columnSettingsExpanded, setColumnSettingsExpanded] = useState(false);
// search state
const [searchValue, setSearchValue] = useState('');
const [searchResults, setSearchResults] = useState<GraphRow[]>([]);
const [searchResultKey, setSearchResultKey] = useState<string | undefined>(undefined);

useEffect(() => {
if (searchValue === '' || searchValue.length < 3 || graphRows.length < 1) {
setSearchResults([]);
setSearchResultKey(undefined);
return;
}

const results = graphRows.filter(row => row.message.toLowerCase().indexOf(searchValue.toLowerCase()) > 0);
setSearchResults(results);

if (
searchResultKey == null ||
(searchResultKey != null && results.findIndex(row => row.sha === searchResultKey) === -1)
) {
setSearchResultKey(results[0]?.sha);
}
}, [searchValue, graphRows]);

const searchPosition: number = useMemo(() => {
if (searchResultKey == null) {
return 1;
}

const idx = searchResults.findIndex(row => row.sha === searchResultKey);
if (idx < 1) {
return 1;
}

return idx + 1;
}, [searchResultKey, searchResults]);

const handleSearchNavigation = (next = true) => {
const rowIndex = searchResultKey != null && searchResults.findIndex(row => row.sha === searchResultKey);
if (rowIndex === false) {
return;
}
if (next && rowIndex < searchResults.length - 1) {
setSearchResultKey(searchResults[rowIndex + 1].sha);
} else if (!next && rowIndex > 0) {
setSearchResultKey(searchResults[rowIndex - 1].sha);
}
};

const handleSearchInput = (e: FormEvent<HTMLInputElement>) => {
const currentValue = e.currentTarget.value;

setSearchValue(currentValue);
};

useLayoutEffect(() => {
if (mainRef.current === null) return;

const setDimensionsDebounced = debounceFrame((width, height) => {
Expand Down Expand Up @@ -478,6 +532,30 @@ export function GraphWrapper({
)}
{renderAlertContent()}
</section>
<header className="titlebar graph-app__header">
<div className="titlebar__group">
<div role="search" className="search-input">
<label htmlFor="titlebar-search">
<span className="codicon codicon-search" aria-label="Search" title="Search"></span>
</label>
<input
id="titlebar-search"
type="search"
spellCheck="false"
placeholder="Search..."
value={searchValue}
onChange={e => handleSearchInput(e)}
/>
</div>
<SearchNav
aria-label="Graph search navigation"
step={searchPosition}
total={searchResults.length}
onPrevious={() => handleSearchNavigation(false)}
onNext={() => handleSearchNavigation(true)}
/>
</div>
</header>
<main
ref={mainRef}
id="main"
Expand Down
72 changes: 72 additions & 0 deletions src/webviews/apps/plus/graph/graph.scss
Expand Up @@ -514,6 +514,64 @@ a {
}
}

.search-input {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 0.8rem;

label {
color: var(--vscode-input-foreground);
}
input {
width: 30rem;
height: 2.4rem;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-background); // var(--vscode-input-foreground);
border-radius: 0.25rem;
padding: {
left: 0.4rem;
right: 0.4rem;
}

&:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}

&::placeholder {
color: var(--vscode-input-placeholderForeground);
}
&:focus {
border-color: var(--vscode-focusBorder);
}
}
}

.titlebar {
background: var(--vscode-titleBar-inactiveBackground);
color: var(--vscode-titleBar-inactiveForeground);
height: 3.6rem;
padding: {
left: 0.8rem;
right: 0.8rem;
}

&,
&__group {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 0.5rem;

> * {
margin: 0;
}
}
}

.graph-app {
--fs-1: 1.1rem;
--fs-2: 1.3rem;
Expand Down Expand Up @@ -548,6 +606,10 @@ a {
backdrop-filter: blur(4px) saturate(0.8);
}

&__header {
flex: none;
}

&__footer {
flex: none;
position: relative;
Expand Down Expand Up @@ -708,3 +770,13 @@ a {
transform: translateX(4900%) scaleX(1);
}
}

.sr-only {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
10 changes: 10 additions & 0 deletions src/webviews/apps/shared/components/search/search-nav-react.tsx
@@ -0,0 +1,10 @@
import { provideReactWrapper } from '@microsoft/fast-react-wrapper';
import React from 'react';
import { SearchNav as nativeComponent } from './search-nav';

export const SearchNav = provideReactWrapper(React).wrap(nativeComponent, {
events: {
onPrevious: 'previous',
onNext: 'next',
},
});
133 changes: 133 additions & 0 deletions src/webviews/apps/shared/components/search/search-nav.ts
@@ -0,0 +1,133 @@
import { attr, css, customElement, FASTElement, html, ref, volatile, when } from '@microsoft/fast-element';
import { numberConverter } from '../converters/number-converter';
import '../codicon';

const template = html<SearchNav>`<template>
<span class="count">
${when(x => x.total < 1, html<SearchNav>`No ${x => x.formattedLabel}`)}
${when(
x => x.total > 0,
html<SearchNav>`<span aria-current="step">${x => x.step}</span> of ${x => x.total}<span class="sr-only">
${x => x.formattedLabel}</span
>`,
)}
</span>
<button
type="button"
class="button"
?disabled="${x => !x.hasPrevious}"
@click="${(x, c) => x.handlePrevious(c.event)}"
>
<code-icon
icon="arrow-up"
aria-label="Go to previous ${x => x.label}"
title="Go to previous ${x => x.label}"
></code-icon>
</button>
<button type="button" class="button" ?disabled="${x => !x.hasNext}" @click="${(x, c) => x.handleNext(c.event)}">
<code-icon
icon="arrow-down"
aria-label="Go to next ${x => x.label}"
title="Go to next ${x => x.label}"
></code-icon>
</button>
</template>`;

const styles = css`
:host {
display: inline-flex;
flex-direction: row;
align-items: center;
/* gap: 0.8rem; */
color: var(--vscode-titleBar-inactiveForeground);
}
:host(:focus) {
outline: 0;
}
.count {
flex: none;
margin-right: 0.4rem;
}
.button {
width: 2.4rem;
height: 2.4rem;
padding: 0;
color: inherit;
border: none;
background: none;
text-align: center;
}
.button:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
.button:not([disabled]) {
cursor: pointer;
}
.button:hover:not([disabled]) {
background-color: var(--vscode-titleBar-activeBackground);
}
.button > code-icon[icon='arrow-up'] {
transform: translateX(-0.1rem);
}
.sr-only {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
`;

@customElement({ name: 'search-nav', template: template, styles: styles })
export class SearchNav extends FASTElement {
@attr({ converter: numberConverter })
total = 0;

@attr({ converter: numberConverter })
step = 0;

@attr
label = 'result';

@volatile
get formattedLabel() {
if (this.total === 1) {
return this.label;
}

return `${this.label}s`;
}

@volatile
get hasPrevious() {
if (this.total === 0) {
return false;
}

return this.step > 1;
}

@volatile
get hasNext() {
if (this.total === 0) {
return false;
}

return this.step < this.total;
}

handlePrevious(_e: Event) {
this.$emit('previous');
}

handleNext(_e: Event) {
this.$emit('next');
}
}
18 changes: 13 additions & 5 deletions yarn.lock
Expand Up @@ -188,12 +188,12 @@
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.4.1.tgz#3f587eec5708692135bc9e94cf396130604979f3"
integrity sha512-qDv4851VFSaBWzpS02cXHclo40jsbAjRXnebNXpm0uVg32kCneZPo9RYVQtrTNICtZ+1wAYHu1ZtxWSWMbKrBw==

"@microsoft/fast-element@^1.10.5", "@microsoft/fast-element@^1.6.2", "@microsoft/fast-element@^1.9.0":
"@microsoft/fast-element@1.10.5", "@microsoft/fast-element@^1.10.5", "@microsoft/fast-element@^1.6.2", "@microsoft/fast-element@^1.9.0":
version "1.10.5"
resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-1.10.5.tgz#0ccedb56bd1fa9d981acb33665d074abb3bf76f5"
integrity sha512-7aqo60dh+ip+NyReRPRiR8ndb4ZX7An3ms6TNhnrdLUtCon4kOv0GVxJdlVjqsSvrk9nOMN58A6Sg6ertt8hXQ==

"@microsoft/fast-foundation@^2.38.0", "@microsoft/fast-foundation@^2.41.1":
"@microsoft/fast-foundation@^2.38.0", "@microsoft/fast-foundation@^2.41.1", "@microsoft/fast-foundation@^2.46.14":
version "2.46.14"
resolved "https://registry.yarnpkg.com/@microsoft/fast-foundation/-/fast-foundation-2.46.14.tgz#75e9b31ba0781f5f437710f1135fa903076d9fb3"
integrity sha512-r97fidZZ7uMjqg/HPYEY7EofZaPS8mCnQWWuvG9oS0JuE1XeLeYD3eMo/mG1Xn68IZc9kztyzA2sGUrm4piq/Q==
Expand All @@ -203,6 +203,14 @@
tabbable "^5.2.0"
tslib "^1.13.0"

"@microsoft/fast-react-wrapper@0.3.14":
version "0.3.14"
resolved "https://registry.yarnpkg.com/@microsoft/fast-react-wrapper/-/fast-react-wrapper-0.3.14.tgz#2288e7fa805e91759cd4e7004eb1044fddcc6563"
integrity sha512-X6B+b8SD7vkdhS3/cnEn2ES17G46MH4LwVK298wfLlOsPnI9VzrlUQXbumFk8aNotQWU5sJDZ85Zgf/+Rzlcdg==
dependencies:
"@microsoft/fast-element" "^1.10.5"
"@microsoft/fast-foundation" "^2.46.14"

"@microsoft/fast-react-wrapper@^0.1.18":
version "0.1.48"
resolved "https://registry.yarnpkg.com/@microsoft/fast-react-wrapper/-/fast-react-wrapper-0.1.48.tgz#aa89c0dfb703c2f71619c536de2342e28b40b8c9"
Expand Down Expand Up @@ -2319,9 +2327,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==

electron-to-chromium@^1.4.251:
version "1.4.256"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz#c735032f412505e8e0482f147a8ff10cfca45bf4"
integrity sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw==
version "1.4.257"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.257.tgz#895dc73c6bb58d1235dc80879ecbca0bcba96e2c"
integrity sha512-C65sIwHqNnPC2ADMfse/jWTtmhZMII+x6ADI9gENzrOiI7BpxmfKFE84WkIEl5wEg+7+SfIkwChDlsd1Erju2A==

emoji-regex@^8.0.0:
version "8.0.0"
Expand Down

0 comments on commit d2bd3da

Please sign in to comment.