Skip to content

Commit 0d8b1c0

Browse files
authored
Fix repository opt-out search highlighting error (#4801)
Signed-off-by: Cintia Sánchez García <cynthiasg@icloud.com>
1 parent a72d8a2 commit 0d8b1c0

7 files changed

Lines changed: 105 additions & 27 deletions

File tree

web/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
"react-markdown": "^10.1.0",
3131
"react-router-dom": "^7.14.0",
3232
"react-syntax-highlighter": "^16.1.1",
33-
"regexify-string": "^1.0.17",
3433
"rehype-github-alerts": "^4.2.0",
3534
"remark-gfm": "^4.0.1",
3635
"remark-parse": "^11.0.0",

web/src/layout/common/SearchRepositories.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('SearchRepositories', () => {
4242
const mockSearch = getMockSearch('1');
4343
vi.mocked(API).searchRepositories.mockResolvedValue(mockSearch);
4444

45-
render(<SearchRepositories {...defaultProps} />);
45+
const { container } = render(<SearchRepositories {...defaultProps} />);
4646

4747
const input = screen.getByRole('textbox', { name: 'Search repositories' });
4848
expect(input).toBeInTheDocument();
@@ -55,6 +55,9 @@ describe('SearchRepositories', () => {
5555
});
5656

5757
expect(await screen.findAllByRole('button')).toHaveLength(3);
58+
const highlightedMatches = container.querySelectorAll('.highlighted');
59+
expect(highlightedMatches).toHaveLength(1);
60+
expect(highlightedMatches[0]).toHaveTextContent('sec');
5861
});
5962

6063
it('selects repo', async () => {

web/src/layout/common/SearchRepositories.tsx

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from 'react';
66
import { FaUser } from 'react-icons/fa';
77
import { FiSearch } from 'react-icons/fi';
88
import { MdBusiness } from 'react-icons/md';
9-
import regexifyString from 'regexify-string';
109

1110
import API from '../../api';
1211
import useOutsideClick from '../../hooks/useOutsideClick';
1312
import { ErrorKind, Repository, SearchQuery } from '../../types';
1413
import alertDispatcher from '../../utils/alertDispatcher';
14+
import regexifyString from '../../utils/regexifyString';
1515
import Loading from './Loading';
1616
import RepositoryIcon from './RepositoryIcon';
1717
import styles from './SearchRepositories.module.css';
@@ -162,6 +162,21 @@ const SearchRepositories = (props: Props) => {
162162
}
163163
};
164164

165+
// Highlights the typed search term in repository names.
166+
const getHighlightedRepositoryName = (repositoryName: string) => {
167+
return regexifyString({
168+
pattern: new RegExp(escapeRegExp(searchName), 'gi'),
169+
decorator: (match: string, index: number) => {
170+
return (
171+
<span key={`match_${repositoryName}_${index}`} className="fw-bold highlighted">
172+
{match}
173+
</span>
174+
);
175+
},
176+
input: repositoryName,
177+
});
178+
};
179+
165180
const cleanTimeout = () => {
166181
if (!isNull(dropdownTimeout)) {
167182
clearTimeout(dropdownTimeout);
@@ -298,23 +313,7 @@ const SearchRepositories = (props: Props) => {
298313
<td className="align-middle">
299314
<div className={styles.truncateWrapper}>
300315
<div className="text-truncate">
301-
{searchName === '' ? (
302-
<>{item.name}</>
303-
) : (
304-
<>
305-
{regexifyString({
306-
pattern: new RegExp(escapeRegExp(searchName), 'gi'),
307-
decorator: (match: string, index: number) => {
308-
return (
309-
<span key={`match_${item.name}_${index}`} className="fw-bold highlighted">
310-
{match}
311-
</span>
312-
);
313-
},
314-
input: item.name,
315-
})}
316-
</>
317-
)}
316+
{searchName === '' ? <>{item.name}</> : <>{getHighlightedRepositoryName(item.name)}</>}
318317
</div>
319318
</div>
320319
</td>

web/src/layout/package/chartTemplates/Template.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import isObject from 'lodash/isObject';
66
import isString from 'lodash/isString';
77
import isUndefined from 'lodash/isUndefined';
88
import { Dispatch, Fragment, memo, SetStateAction, useContext, useEffect, useState } from 'react';
9-
import * as regexifyStringModule from 'regexify-string';
109

1110
import { AppCtx } from '../../../context/AppCtx';
1211
import { ChartTemplate, ChartTemplateSpecialType, DefinedTemplate, DefinedTemplatesList } from '../../../types';
1312
import processHelmTemplate from '../../../utils/processHelmTemplate';
13+
import regexifyString from '../../../utils/regexifyString';
1414
import AutoresizeTextarea from '../../common/AutoresizeTextarea';
1515
import builtInDefinitions from './builtIn.json';
1616
import functionsDefinitions from './functions.json';
@@ -32,7 +32,6 @@ const SPECIAL_CHARACTERS = /[^|({})-]+/;
3232
const TOKENIZE_RE = /[^\s"']+|"([^"]*)"|'([^']*)/g;
3333
const INITIAL_HELPER_COMMENT = /{{\/\*|{{- \/\*/;
3434
const FINAL_HELPER_COMMENT = /\*\/}}|\*\/ -}}$/;
35-
const regexifyString = regexifyStringModule.default;
3635

3736
const Template = (props: Props) => {
3837
const { ctx } = useContext(AppCtx);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { render, screen } from '@testing-library/react';
2+
3+
import regexifyString from './regexifyString';
4+
5+
describe('regexifyString', () => {
6+
it('decorates matches', () => {
7+
render(
8+
<>
9+
{regexifyString({
10+
pattern: /hub/gi,
11+
decorator: (match, index) => {
12+
return <span key={`match_${index}`}>{match}</span>;
13+
},
14+
input: 'artifact-hub security-hub',
15+
})}
16+
</>
17+
);
18+
19+
expect(screen.getAllByText('hub')).toHaveLength(2);
20+
});
21+
22+
it('passes the match result to the decorator', () => {
23+
render(
24+
<>
25+
{regexifyString({
26+
pattern: /repo-(\d+)/,
27+
decorator: (_match, _index, result) => {
28+
return <span>{result[1]}</span>;
29+
},
30+
input: 'repo-1 repo-2',
31+
})}
32+
</>
33+
);
34+
35+
expect(screen.getByText('1')).toBeInTheDocument();
36+
expect(screen.getByText('2')).toBeInTheDocument();
37+
});
38+
});

web/src/utils/regexifyString.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { ReactNode } from 'react';
2+
3+
interface RegexifyStringProps {
4+
pattern: RegExp;
5+
decorator: (match: string, index: number, result: RegExpExecArray) => ReactNode;
6+
input: string;
7+
}
8+
9+
/**
10+
* Splits text by a regex and decorates each match for React rendering.
11+
*/
12+
const regexifyString = (props: RegexifyStringProps): ReactNode[] => {
13+
const { pattern, decorator, input } = props;
14+
// Work on a global copy so callers can pass reusable regex constants.
15+
const patternFlags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
16+
const globalPattern = new RegExp(pattern.source, patternFlags);
17+
const output: ReactNode[] = [];
18+
let match = globalPattern.exec(input);
19+
let matchIndex = 0;
20+
let previousMatchEnd = 0;
21+
22+
while (match !== null) {
23+
const matchStart = match.index;
24+
const matchValue = match[0];
25+
// Preserve text between matches so React can render the original input.
26+
output.push(input.substring(previousMatchEnd, matchStart));
27+
output.push(decorator(matchValue, matchIndex, match));
28+
29+
// Avoid infinite loops when a regex can match an empty string.
30+
if (matchValue.length === 0) {
31+
globalPattern.lastIndex += 1;
32+
}
33+
previousMatchEnd = globalPattern.lastIndex;
34+
matchIndex += 1;
35+
match = globalPattern.exec(input);
36+
}
37+
38+
if (previousMatchEnd < input.length) {
39+
output.push(input.substring(previousMatchEnd));
40+
}
41+
42+
return output;
43+
};
44+
45+
export default regexifyString;

web/yarn.lock

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4591,11 +4591,6 @@ refractor@^5.0.0:
45914591
hastscript "^9.0.0"
45924592
parse-entities "^4.0.0"
45934593

4594-
regexify-string@^1.0.17:
4595-
version "1.0.19"
4596-
resolved "https://registry.yarnpkg.com/regexify-string/-/regexify-string-1.0.19.tgz#74fab49b348b8faffbd948218ea3a09e25303010"
4597-
integrity sha512-EREOggl31J6v2Hk3ksPuOof0DMq5QhFfVQ7iDaGQ6BeA1QcrV4rhGvwCES5a72ITMmLBDAOb6cOWbn8/Ja82Ig==
4598-
45994594
rehype-github-alerts@^4.2.0:
46004595
version "4.2.0"
46014596
resolved "https://registry.yarnpkg.com/rehype-github-alerts/-/rehype-github-alerts-4.2.0.tgz#876f2a5aefcb5f80637077490dc65b14183b1397"

0 commit comments

Comments
 (0)