From d2bd3dae4997cbec894712495d52446e867b0c8f Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Tue, 20 Sep 2022 15:01:21 -0400 Subject: [PATCH] Adds search controls to the graph --- package.json | 3 +- src/webviews/apps/plus/graph/GraphWrapper.tsx | 82 ++++++++++++- src/webviews/apps/plus/graph/graph.scss | 72 +++++++++++ .../shared/components/search/search-nav-react.tsx | 10 ++ .../apps/shared/components/search/search-nav.ts | 133 +++++++++++++++++++++ yarn.lock | 18 ++- 6 files changed, 310 insertions(+), 8 deletions(-) create mode 100644 src/webviews/apps/shared/components/search/search-nav-react.tsx create mode 100644 src/webviews/apps/shared/components/search/search-nav.ts diff --git a/package.json b/package.json index 1325bd2..2faea47 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 81dad54..db2fdfd 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -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'; @@ -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'; @@ -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([]); + const [searchResultKey, setSearchResultKey] = useState(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) => { + const currentValue = e.currentTarget.value; + + setSearchValue(currentValue); + }; + + useLayoutEffect(() => { if (mainRef.current === null) return; const setDimensionsDebounced = debounceFrame((width, height) => { @@ -478,6 +532,30 @@ export function GraphWrapper({ )} {renderAlertContent()} +
+
+
+ + handleSearchInput(e)} + /> +
+ handleSearchNavigation(false)} + onNext={() => handleSearchNavigation(true)} + /> +
+
* { + margin: 0; + } + } +} + .graph-app { --fs-1: 1.1rem; --fs-2: 1.3rem; @@ -548,6 +606,10 @@ a { backdrop-filter: blur(4px) saturate(0.8); } + &__header { + flex: none; + } + &__footer { flex: none; position: relative; @@ -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; +} diff --git a/src/webviews/apps/shared/components/search/search-nav-react.tsx b/src/webviews/apps/shared/components/search/search-nav-react.tsx new file mode 100644 index 0000000..3282739 --- /dev/null +++ b/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', + }, +}); diff --git a/src/webviews/apps/shared/components/search/search-nav.ts b/src/webviews/apps/shared/components/search/search-nav.ts new file mode 100644 index 0000000..04e7ab7 --- /dev/null +++ b/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``; + +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'); + } +} diff --git a/yarn.lock b/yarn.lock index 4407ee3..9657fcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== @@ -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" @@ -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"