Implemented project settings page, search issues modal, general refactoring
This commit is contained in:
77
client/src/Project/IssueSearch/NoResultsSvg.jsx
Normal file
77
client/src/Project/IssueSearch/NoResultsSvg.jsx
Normal file
File diff suppressed because one or more lines are too long
96
client/src/Project/IssueSearch/Styles.js
Normal file
96
client/src/Project/IssueSearch/Styles.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { InputDebounced, Spinner, Icon } from 'shared/components';
|
||||
|
||||
export const IssueSearch = styled.div`
|
||||
padding: 25px 35px;
|
||||
`;
|
||||
|
||||
export const SearchInputCont = styled.div`
|
||||
position: relative;
|
||||
padding-right: 30px;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
export const SearchInputDebounced = styled(InputDebounced)`
|
||||
height: 40px;
|
||||
input {
|
||||
padding: 0 0 0 32px;
|
||||
border: none;
|
||||
border-bottom: 2px solid ${color.primary};
|
||||
background: #fff;
|
||||
${font.size(21)}
|
||||
&:focus,
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid ${color.primary};
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SearchIcon = styled(Icon)`
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
color: ${color.textMedium};
|
||||
`;
|
||||
|
||||
export const SearchSpinner = styled(Spinner)`
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 30px;
|
||||
`;
|
||||
|
||||
export const Issue = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.1s;
|
||||
${mixin.clickable}
|
||||
&:hover {
|
||||
background: ${color.backgroundLight};
|
||||
}
|
||||
`;
|
||||
|
||||
export const IssueData = styled.div`
|
||||
padding-left: 15px;
|
||||
`;
|
||||
|
||||
export const IssueTitle = styled.div`
|
||||
color: ${color.textDark};
|
||||
${font.size(15)}
|
||||
`;
|
||||
|
||||
export const IssueTypeId = styled.div`
|
||||
text-transform: uppercase;
|
||||
color: ${color.textMedium};
|
||||
${font.size(12.5)}
|
||||
`;
|
||||
|
||||
export const SectionTitle = styled.div`
|
||||
padding-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
color: ${color.textMedium};
|
||||
${font.bold}
|
||||
${font.size(11.5)}
|
||||
`;
|
||||
|
||||
export const NoResults = styled.div`
|
||||
padding-top: 50px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const NoResultsTitle = styled.div`
|
||||
padding-top: 30px;
|
||||
${font.medium}
|
||||
${font.size(20)}
|
||||
`;
|
||||
|
||||
export const NoResultsTip = styled.div`
|
||||
padding-top: 10px;
|
||||
${font.size(15)}
|
||||
`;
|
||||
101
client/src/Project/IssueSearch/index.jsx
Normal file
101
client/src/Project/IssueSearch/index.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { sortByNewest } from 'shared/utils/javascript';
|
||||
import { IssueTypeIcon } from 'shared/components';
|
||||
|
||||
import NoResultsSVG from './NoResultsSvg';
|
||||
import {
|
||||
IssueSearch,
|
||||
SearchInputCont,
|
||||
SearchInputDebounced,
|
||||
SearchIcon,
|
||||
SearchSpinner,
|
||||
Issue,
|
||||
IssueData,
|
||||
IssueTitle,
|
||||
IssueTypeId,
|
||||
SectionTitle,
|
||||
NoResults,
|
||||
NoResultsTitle,
|
||||
NoResultsTip,
|
||||
} from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
project: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const ProjectIssueSearch = ({ project }) => {
|
||||
const [isSearchTermEmpty, setIsSearchTermEmpty] = useState(true);
|
||||
|
||||
const [{ data, isLoading }, fetchIssues] = useApi.get('/issues', {}, { lazy: true });
|
||||
|
||||
const matchingIssues = get(data, 'issues', []);
|
||||
|
||||
const recentIssues = sortByNewest(project.issues, 'createdAt').slice(0, 10);
|
||||
|
||||
const handleSearchChange = value => {
|
||||
const searchTerm = value.trim();
|
||||
|
||||
setIsSearchTermEmpty(!searchTerm);
|
||||
|
||||
if (searchTerm) {
|
||||
fetchIssues({ searchTerm });
|
||||
}
|
||||
};
|
||||
|
||||
const renderIssue = issue => (
|
||||
<Link key={issue.id} to={`/project/board/issues/${issue.id}`}>
|
||||
<Issue>
|
||||
<IssueTypeIcon type={issue.type} size={25} />
|
||||
<IssueData>
|
||||
<IssueTitle>{issue.title}</IssueTitle>
|
||||
<IssueTypeId>{`${issue.type}-${issue.id}`}</IssueTypeId>
|
||||
</IssueData>
|
||||
</Issue>
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<IssueSearch>
|
||||
<SearchInputCont>
|
||||
<SearchInputDebounced
|
||||
autoFocus
|
||||
placeholder="Search issues by summary, description..."
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<SearchIcon type="search" size={22} />
|
||||
{isLoading && <SearchSpinner />}
|
||||
</SearchInputCont>
|
||||
|
||||
{isSearchTermEmpty && recentIssues.length > 0 && (
|
||||
<>
|
||||
<SectionTitle>Recent Issues</SectionTitle>
|
||||
{recentIssues.map(renderIssue)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isSearchTermEmpty && matchingIssues.length > 0 && (
|
||||
<>
|
||||
<SectionTitle>Matching Issues</SectionTitle>
|
||||
{matchingIssues.map(renderIssue)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isSearchTermEmpty && !isLoading && matchingIssues.length === 0 && (
|
||||
<NoResults>
|
||||
<NoResultsSVG />
|
||||
<NoResultsTitle>We couldn't find anything matching your search</NoResultsTitle>
|
||||
<NoResultsTip>Try again with a different term.</NoResultsTip>
|
||||
</NoResults>
|
||||
)}
|
||||
</IssueSearch>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectIssueSearch.propTypes = propTypes;
|
||||
|
||||
export default ProjectIssueSearch;
|
||||
Reference in New Issue
Block a user