Browse Source

SONAR-14057 Highlight search query in results

tags/8.6.0.39681
Jeremy Davis 3 years ago
parent
commit
836733b9c9

+ 4
- 0
server/sonar-web/src/main/js/app/styles/init/type.css View File

@@ -140,6 +140,10 @@ strong {
font-weight: 600;
}

.underline {
text-decoration: underline;
}

mark {
background: none;
color: var(--baseFontColor);

+ 35
- 11
server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx View File

@@ -40,14 +40,40 @@ export interface AzureProjectAccordionProps {
onSelectRepository: (repository: AzureRepository) => void;
project: AzureProject;
repositories?: AzureRepository[];
searchQuery?: string;
selectedRepository?: AzureRepository;
startsOpen: boolean;
}

const PAGE_SIZE = 30;

function highlight(text: string, term?: string, underline = false) {
if (!term || !text.toLowerCase().includes(term.toLowerCase())) {
return text;
}

// Capture only the first occurence by using a capturing group to get
// everything after the first occurence
const [pre, found, post] = text.split(new RegExp(`(${term})(.*)`, 'i'));
return (
<>
{pre}
<strong className={classNames({ underline })}>{found}</strong>
{post}
</>
);
}

export default function AzureProjectAccordion(props: AzureProjectAccordionProps) {
const { importing, loading, startsOpen, project, repositories = [], selectedRepository } = props;
const {
importing,
loading,
startsOpen,
project,
repositories = [],
searchQuery,
selectedRepository
} = props;

const [open, setOpen] = React.useState(startsOpen);
const handleClick = () => {
@@ -70,7 +96,7 @@ export default function AzureProjectAccordion(props: AzureProjectAccordionProps)
})}
onClick={handleClick}
open={open}
title={<h3 title={project.description}>{project.name}</h3>}>
title={<h3 title={project.description}>{highlight(project.name, searchQuery, true)}</h3>}>
{open && (
<DeferredSpinner loading={loading}>
{/* The extra loading guard is to prevent the flash of the Alert */}
@@ -97,18 +123,16 @@ export default function AzureProjectAccordion(props: AzureProjectAccordionProps)
<div className="display-flex-wrap">
{limitedRepositories.map(repo => (
<div
className="display-flex-start spacer-right spacer-bottom create-project-azdo-repo"
className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
key={repo.name}>
{repo.sqProjectKey ? (
<>
<CheckIcon className="spacer-right" fill={colors.green} size={14} />
<div className="overflow-hidden">
<div className="little-spacer-bottom text-ellipsis">
<strong title={repo.sqProjectName}>
<Link to={getProjectUrl(repo.sqProjectKey)}>
{repo.sqProjectName}
</Link>
</strong>
<Link to={getProjectUrl(repo.sqProjectKey)} title={repo.sqProjectName}>
{highlight(repo.sqProjectName || repo.name, searchQuery)}
</Link>
</div>
<em>{translate('onboarding.create_project.repository_imported')}</em>
</div>
@@ -120,9 +144,9 @@ export default function AzureProjectAccordion(props: AzureProjectAccordionProps)
disabled={importing}
onCheck={() => props.onSelectRepository(repo)}
value={repo.name}>
<strong className="text-ellipsis" title={repo.name}>
{repo.name}
</strong>
<span className="text-ellipsis" title={repo.name}>
{highlight(repo.name, searchQuery)}
</span>
</Radio>
)}
</div>

+ 9
- 2
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx View File

@@ -48,6 +48,7 @@ interface State {
repositories: T.Dict<AzureRepository[]>;
searching?: boolean;
searchResults?: T.Dict<AzureRepository[]>;
searchQuery?: string;
selectedRepository?: AzureRepository;
settings?: AlmSettingsInstance;
submittingToken?: boolean;
@@ -184,7 +185,7 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
}

if (searchQuery.length === 0) {
this.setState({ searchResults: undefined });
this.setState({ searchResults: undefined, searchQuery: undefined });
return;
}

@@ -195,7 +196,11 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
.catch(() => []);

if (this.mounted) {
this.setState({ searching: false, searchResults: groupBy(results, 'projectName') });
this.setState({
searching: false,
searchResults: groupBy(results, 'projectName'),
searchQuery
});
}
};

@@ -277,6 +282,7 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
repositories,
searching,
searchResults,
searchQuery,
selectedRepository,
settings,
submittingToken,
@@ -298,6 +304,7 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
repositories={repositories}
searching={searching}
searchResults={searchResults}
searchQuery={searchQuery}
selectedRepository={selectedRepository}
settings={settings}
showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}

+ 5
- 2
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx View File

@@ -44,6 +44,7 @@ export interface AzureProjectCreateRendererProps {
repositories: T.Dict<AzureRepository[]>;
searching?: boolean;
searchResults?: T.Dict<AzureRepository[]>;
searchQuery?: string;
selectedRepository?: AzureRepository;
settings?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean;
@@ -61,6 +62,7 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend
repositories,
searching,
searchResults,
searchQuery,
selectedRepository,
settings,
showPersonalAccessTokenForm,
@@ -99,7 +101,7 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend

{loading && <i className="spinner" />}

{!loading && !settings && (
{!loading && !(settings && settings.url) && (
<WrongBindingCountAlert alm={AlmKeys.Azure} canAdmin={!!canAdmin} />
)}

@@ -119,7 +121,7 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend
<div className="huge-spacer-bottom">
<SearchBox
onChange={props.onSearch}
placeholder={translate('onboarding.create_project.search_repositories_by_name')}
placeholder={translate('onboarding.create_project.search_projects_repositories')}
/>
</div>
<DeferredSpinner loading={Boolean(searching)}>
@@ -131,6 +133,7 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend
projects={projects}
repositories={repositories}
searchResults={searchResults}
searchQuery={searchQuery}
selectedRepository={selectedRepository}
/>
</DeferredSpinner>

+ 3
- 0
server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx View File

@@ -35,6 +35,7 @@ export interface AzureProjectsListProps {
projects?: AzureProject[];
repositories: T.Dict<AzureRepository[]>;
searchResults?: T.Dict<AzureRepository[]>;
searchQuery?: string;
selectedRepository?: AzureRepository;
}

@@ -47,6 +48,7 @@ export default function AzureProjectsList(props: AzureProjectsListProps) {
projects = [],
repositories,
searchResults,
searchQuery,
selectedRepository
} = props;

@@ -100,6 +102,7 @@ export default function AzureProjectsList(props: AzureProjectsListProps) {
project={p}
repositories={searchResults ? searchResults[p.name] : repositories[p.name]}
selectedRepository={selectedRepository}
searchQuery={searchQuery}
startsOpen={searchResults !== undefined || i === 0}
/>
))}

+ 13
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectAccordion-test.tsx View File

@@ -40,6 +40,19 @@ it('should render correctly', () => {
expect(shallowRender({ importing: true, repositories: [mockAzureRepository()] })).toMatchSnapshot(
'importing'
);
expect(
shallowRender({
repositories: [
mockAzureRepository({ name: 'this repo is the best' }),
mockAzureRepository({
name: 'This is a repo with class',
sqProjectKey: 'sq-key',
sqProjectName: 'SQ Name'
})
],
searchQuery: 'repo'
})
).toMatchSnapshot('search results');
});

it('should open when clicked', () => {

+ 2
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx View File

@@ -157,6 +157,7 @@ it('should handle searching for repositories', async () => {
await waitAndUpdate(wrapper);
expect(wrapper.state().searching).toBe(false);
expect(wrapper.state().searchResults).toEqual({ [repositories[0].projectName]: repositories });
expect(wrapper.state().searchQuery).toBe(query);

// Ignore opening a project when search results are displayed
(getAzureRepositories as jest.Mock).mockClear();
@@ -169,6 +170,7 @@ it('should handle searching for repositories', async () => {
wrapper.instance().handleSearchRepositories('');
expect(searchAzureRepositories).not.toBeCalled();
expect(wrapper.state().searchResults).toBeUndefined();
expect(wrapper.state().searchQuery).toBeUndefined();
});

it('should select and import a repository', async () => {

+ 112
- 24
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap View File

@@ -35,7 +35,7 @@ exports[`should render correctly: importing 1`] = `
className="display-flex-wrap"
>
<div
className="display-flex-start spacer-right spacer-bottom create-project-azdo-repo"
className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
key="Azure repo 1"
>
<Radio
@@ -45,12 +45,12 @@ exports[`should render correctly: importing 1`] = `
onCheck={[Function]}
value="Azure repo 1"
>
<strong
<span
className="text-ellipsis"
title="Azure repo 1"
>
Azure repo 1
</strong>
</span>
</Radio>
</div>
</div>
@@ -91,6 +91,97 @@ exports[`should render correctly: loading 1`] = `
</BoxedGroupAccordion>
`;

exports[`should render correctly: search results 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom open"
onClick={[Function]}
open={true}
title={
<h3
title="Azure Project"
>
azure-project-1
</h3>
}
>
<DeferredSpinner
loading={false}
>
<div
className="display-flex-wrap"
>
<div
className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
key="this repo is the best"
>
<Radio
checked={false}
className="overflow-hidden"
disabled={false}
onCheck={[Function]}
value="this repo is the best"
>
<span
className="text-ellipsis"
title="this repo is the best"
>
this
<strong
className=""
>
repo
</strong>
is the best
</span>
</Radio>
</div>
<div
className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
key="This is a repo with class"
>
<CheckIcon
className="spacer-right"
fill="#00aa00"
size={14}
/>
<div
className="overflow-hidden"
>
<div
className="little-spacer-bottom text-ellipsis"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
title="SQ Name"
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "sq-key",
},
}
}
>
SQ Name
</Link>
</div>
<em>
onboarding.create_project.repository_imported
</em>
</div>
</div>
</div>
<ListFooter
count={2}
loadMore={[Function]}
total={2}
/>
</DeferredSpinner>
</BoxedGroupAccordion>
`;

exports[`should render correctly: with repositories 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom open"
@@ -111,7 +202,7 @@ exports[`should render correctly: with repositories 1`] = `
className="display-flex-wrap"
>
<div
className="display-flex-start spacer-right spacer-bottom create-project-azdo-repo"
className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
key="Azure repo 1"
>
<Radio
@@ -121,16 +212,16 @@ exports[`should render correctly: with repositories 1`] = `
onCheck={[Function]}
value="Azure repo 1"
>
<strong
<span
className="text-ellipsis"
title="Azure repo 1"
>
Azure repo 1
</strong>
</span>
</Radio>
</div>
<div
className="display-flex-start spacer-right spacer-bottom create-project-azdo-repo"
className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
key="Azure repo 1"
>
<CheckIcon
@@ -144,25 +235,22 @@ exports[`should render correctly: with repositories 1`] = `
<div
className="little-spacer-bottom text-ellipsis"
>
<strong
<Link
onlyActiveOnIndex={false}
style={Object {}}
title="SQ Name"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "sq-key",
},
}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "sq-key",
},
}
>
SQ Name
</Link>
</strong>
}
>
SQ Name
</Link>
</div>
<em>
onboarding.create_project.repository_imported

+ 9
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap View File

@@ -115,12 +115,16 @@ exports[`should render correctly: project list 1`] = `
</span>
}
/>
<WrongBindingCountAlert
alm="azure"
canAdmin={true}
/>
<div
className="huge-spacer-bottom"
>
<SearchBox
onChange={[MockFunction]}
placeholder="onboarding.create_project.search_repositories_by_name"
placeholder="onboarding.create_project.search_projects_repositories"
/>
</div>
<DeferredSpinner
@@ -172,6 +176,10 @@ exports[`should render correctly: token form 1`] = `
</span>
}
/>
<WrongBindingCountAlert
alm="azure"
canAdmin={true}
/>
<div
className="display-flex-justify-center"
>

+ 3
- 1
server/sonar-web/src/main/js/apps/create/project/style.css View File

@@ -35,8 +35,10 @@
}

.create-project-azdo-repo {
width: 250px;
width: 410px;
min-height: 40px;
box-sizing: border-box;
margin-right: auto;
}

.create-project-import-bbs .open .boxed-group-header {

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -3239,6 +3239,7 @@ onboarding.create_project.display_name.help=Some scanners might override the val
onboarding.create_project.repository_imported=Already set up
onboarding.create_project.see_project=See the project
onboarding.create_project.search_repositories_by_name=Search for repository name starting with...
onboarding.create_project.search_projects_repositories=Search for projects and repositories
onboarding.create_project.search_repositories=Search for a repository
onboarding.create_project.select_repositories=Select repositories
onboarding.create_project.select_all_repositories=Select all available repositories

Loading…
Cancel
Save