@@ -140,6 +140,10 @@ strong { | |||
font-weight: 600; | |||
} | |||
.underline { | |||
text-decoration: underline; | |||
} | |||
mark { | |||
background: none; | |||
color: var(--baseFontColor); |
@@ -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> |
@@ -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)} |
@@ -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> |
@@ -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} | |||
/> | |||
))} |
@@ -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', () => { |
@@ -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 () => { |
@@ -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 |
@@ -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" | |||
> |
@@ -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 { |
@@ -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 |