|
@@ -14,106 +14,145 @@ import {Organization, SavedSearch} from 'app/types';
|
|
|
|
|
|
import {getSortLabel} from './utils';
|
|
|
|
|
|
+type MenuItemProps = Omit<Props, 'savedSearchList'> & {
|
|
|
+ search: SavedSearch;
|
|
|
+ isLast: boolean;
|
|
|
+};
|
|
|
+
|
|
|
+function SavedSearchMenuItem({
|
|
|
+ organization,
|
|
|
+ onSavedSearchSelect,
|
|
|
+ onSavedSearchDelete,
|
|
|
+ search,
|
|
|
+ query,
|
|
|
+ sort,
|
|
|
+ isLast,
|
|
|
+}: MenuItemProps) {
|
|
|
+ return (
|
|
|
+ <Tooltip
|
|
|
+ title={
|
|
|
+ <React.Fragment>
|
|
|
+ {`${search.name} \u2022 `}
|
|
|
+ <TooltipSearchQuery>{search.query}</TooltipSearchQuery>
|
|
|
+ {` \u2022 `}
|
|
|
+ {t('Sort: ')}
|
|
|
+ {getSortLabel(search.sort)}
|
|
|
+ </React.Fragment>
|
|
|
+ }
|
|
|
+ containerDisplayMode="block"
|
|
|
+ delay={1000}
|
|
|
+ >
|
|
|
+ <StyledMenuItem
|
|
|
+ isActive={search.query === query && search.sort === sort}
|
|
|
+ isLast={isLast}
|
|
|
+ >
|
|
|
+ <MenuItemLink tabIndex={-1} onClick={() => onSavedSearchSelect(search)}>
|
|
|
+ <SearchTitle>{search.name}</SearchTitle>
|
|
|
+ <SearchQueryContainer>
|
|
|
+ <SearchQuery>{search.query}</SearchQuery>
|
|
|
+ <SearchSort>
|
|
|
+ {t('Sort: ')}
|
|
|
+ {getSortLabel(search.sort)}
|
|
|
+ </SearchSort>
|
|
|
+ </SearchQueryContainer>
|
|
|
+ </MenuItemLink>
|
|
|
+ {search.isGlobal === false && search.isPinned === false && (
|
|
|
+ <Access
|
|
|
+ organization={organization}
|
|
|
+ access={['org:write']}
|
|
|
+ renderNoAccessMessage={false}
|
|
|
+ >
|
|
|
+ <Confirm
|
|
|
+ onConfirm={() => onSavedSearchDelete(search)}
|
|
|
+ message={t('Are you sure you want to delete this saved search?')}
|
|
|
+ stopPropagation
|
|
|
+ >
|
|
|
+ <DeleteButton
|
|
|
+ borderless
|
|
|
+ title={t('Delete this saved search')}
|
|
|
+ icon={<IconDelete />}
|
|
|
+ label={t('delete')}
|
|
|
+ size="zero"
|
|
|
+ />
|
|
|
+ </Confirm>
|
|
|
+ </Access>
|
|
|
+ )}
|
|
|
+ </StyledMenuItem>
|
|
|
+ </Tooltip>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
type Props = {
|
|
|
- organization: Organization;
|
|
|
savedSearchList: SavedSearch[];
|
|
|
+ organization: Organization;
|
|
|
onSavedSearchSelect: (savedSearch: SavedSearch) => void;
|
|
|
onSavedSearchDelete: (savedSearch: SavedSearch) => void;
|
|
|
sort: string;
|
|
|
query?: string;
|
|
|
};
|
|
|
|
|
|
-function SavedSearchMenu({
|
|
|
- savedSearchList,
|
|
|
- onSavedSearchDelete,
|
|
|
- onSavedSearchSelect,
|
|
|
- organization,
|
|
|
- sort,
|
|
|
- query,
|
|
|
-}: Props) {
|
|
|
- if (savedSearchList.length === 0) {
|
|
|
- return <EmptyItem>{t("There don't seem to be any saved searches yet.")}</EmptyItem>;
|
|
|
- }
|
|
|
+function SavedSearchMenu({savedSearchList, ...props}: Props) {
|
|
|
+ const savedSearches = savedSearchList.filter(search => !search.isGlobal);
|
|
|
+ const commonSearches = savedSearchList.filter(search => search.isGlobal);
|
|
|
|
|
|
return (
|
|
|
<React.Fragment>
|
|
|
- {savedSearchList.map((search, index) => (
|
|
|
- <Tooltip
|
|
|
- title={
|
|
|
- <React.Fragment>
|
|
|
- {`${search.name} \u2022 `}
|
|
|
- <TooltipSearchQuery>{search.query}</TooltipSearchQuery>
|
|
|
- {` \u2022 `}
|
|
|
- {t('Sort: ')}
|
|
|
- {getSortLabel(search.sort)}
|
|
|
- </React.Fragment>
|
|
|
- }
|
|
|
- containerDisplayMode="block"
|
|
|
- delay={1000}
|
|
|
- key={search.id}
|
|
|
- >
|
|
|
- <StyledMenuItem
|
|
|
- isActive={search.query === query && search.sort === sort}
|
|
|
- last={index === savedSearchList.length - 1}
|
|
|
- >
|
|
|
- <MenuItemLink tabIndex={-1} onClick={() => onSavedSearchSelect(search)}>
|
|
|
- <SearchTitle>{search.name}</SearchTitle>
|
|
|
- <SearchQuery>{search.query}</SearchQuery>
|
|
|
- <SearchSort>
|
|
|
- {t('Sort: ')}
|
|
|
- {getSortLabel(search.sort)}
|
|
|
- </SearchSort>
|
|
|
- </MenuItemLink>
|
|
|
- {search.isGlobal === false && search.isPinned === false && (
|
|
|
- <Access
|
|
|
- organization={organization}
|
|
|
- access={['org:write']}
|
|
|
- renderNoAccessMessage={false}
|
|
|
- >
|
|
|
- <Confirm
|
|
|
- onConfirm={() => onSavedSearchDelete(search)}
|
|
|
- message={t('Are you sure you want to delete this saved search?')}
|
|
|
- stopPropagation
|
|
|
- >
|
|
|
- <DeleteButton
|
|
|
- borderless
|
|
|
- title={t('Delete this saved search')}
|
|
|
- icon={<IconDelete />}
|
|
|
- label={t('delete')}
|
|
|
- size="zero"
|
|
|
- />
|
|
|
- </Confirm>
|
|
|
- </Access>
|
|
|
- )}
|
|
|
- </StyledMenuItem>
|
|
|
- </Tooltip>
|
|
|
- ))}
|
|
|
+ <MenuHeader>{t('Saved Searches')}</MenuHeader>
|
|
|
+ {savedSearches.length === 0 ? (
|
|
|
+ <EmptyItem>{t('No saved searches yet.')}</EmptyItem>
|
|
|
+ ) : (
|
|
|
+ savedSearches.map((search, index) => (
|
|
|
+ <SavedSearchMenuItem
|
|
|
+ key={search.id}
|
|
|
+ search={search}
|
|
|
+ isLast={index === savedSearches.length - 1}
|
|
|
+ {...props}
|
|
|
+ />
|
|
|
+ ))
|
|
|
+ )}
|
|
|
+ <SecondaryMenuHeader>{t('Pre-built searches')}</SecondaryMenuHeader>
|
|
|
+ {/* Could only happen on self-hosted */}
|
|
|
+ {commonSearches.length === 0 ? (
|
|
|
+ <EmptyItem>{t('No pre-built searches yet.')}</EmptyItem>
|
|
|
+ ) : (
|
|
|
+ commonSearches.map((search, index) => (
|
|
|
+ <SavedSearchMenuItem
|
|
|
+ key={search.id}
|
|
|
+ search={search}
|
|
|
+ isLast={index === commonSearches.length - 1}
|
|
|
+ {...props}
|
|
|
+ />
|
|
|
+ ))
|
|
|
+ )}
|
|
|
</React.Fragment>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export default SavedSearchMenu;
|
|
|
|
|
|
-const SearchTitle = styled('strong')`
|
|
|
+const SearchTitle = styled('div')`
|
|
|
color: ${p => p.theme.textColor};
|
|
|
+ ${overflowEllipsis}
|
|
|
+`;
|
|
|
|
|
|
- &:after {
|
|
|
- content: ' \u2022 ';
|
|
|
- }
|
|
|
+const SearchQueryContainer = styled('div')`
|
|
|
+ font-size: ${p => p.theme.fontSizeExtraSmall};
|
|
|
+ line-height: 1;
|
|
|
+ ${overflowEllipsis}
|
|
|
`;
|
|
|
|
|
|
const SearchQuery = styled('code')`
|
|
|
- color: ${p => p.theme.textColor};
|
|
|
+ color: ${p => p.theme.subText};
|
|
|
+ font-size: ${p => p.theme.fontSizeExtraSmall};
|
|
|
padding: 0;
|
|
|
background: inherit;
|
|
|
`;
|
|
|
|
|
|
const SearchSort = styled('span')`
|
|
|
color: ${p => p.theme.subText};
|
|
|
- font-size: ${p => p.theme.fontSizeSmall};
|
|
|
-
|
|
|
+ font-size: ${p => p.theme.fontSizeExtraSmall};
|
|
|
&:before {
|
|
|
- font-size: ${p => p.theme.fontSizeMedium};
|
|
|
+ font-size: ${p => p.theme.fontSizeExtraSmall};
|
|
|
color: ${p => p.theme.textColor};
|
|
|
content: ' \u2022 ';
|
|
|
}
|
|
@@ -137,11 +176,31 @@ const DeleteButton = styled(Button)`
|
|
|
}
|
|
|
`;
|
|
|
|
|
|
-const StyledMenuItem = styled(MenuItem)<{isActive: boolean; last: boolean}>`
|
|
|
- border-bottom: ${p => (!p.last ? `1px solid ${p.theme.innerBorder}` : null)};
|
|
|
+const MenuHeader = styled('div')`
|
|
|
+ align-items: center;
|
|
|
+ color: ${p => p.theme.gray400};
|
|
|
+ font-weight: 400;
|
|
|
+ background: ${p => p.theme.backgroundSecondary};
|
|
|
+ line-height: 0.75;
|
|
|
+ padding: ${space(1.5)};
|
|
|
+ border-bottom: 1px solid ${p => p.theme.border};
|
|
|
+ border-radius: ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0 0;
|
|
|
+`;
|
|
|
+
|
|
|
+const SecondaryMenuHeader = styled(MenuHeader)`
|
|
|
+ border-top: 1px solid ${p => p.theme.border};
|
|
|
+ border-radius: 0;
|
|
|
+`;
|
|
|
+
|
|
|
+const StyledMenuItem = styled(MenuItem)<{isActive: boolean; isLast: boolean}>`
|
|
|
+ border-bottom: ${p => (!p.isLast ? `1px solid ${p.theme.innerBorder}` : null)};
|
|
|
font-size: ${p => p.theme.fontSizeMedium};
|
|
|
padding: 0;
|
|
|
|
|
|
+ & > span > a {
|
|
|
+ padding: ${space(0.75)} 0 ${space(1)} 0;
|
|
|
+ }
|
|
|
+
|
|
|
${p =>
|
|
|
p.isActive &&
|
|
|
`
|
|
@@ -173,6 +232,6 @@ const MenuItemLink = styled('a')`
|
|
|
`;
|
|
|
|
|
|
const EmptyItem = styled('li')`
|
|
|
- padding: 8px 10px 5px;
|
|
|
- font-style: italic;
|
|
|
+ padding: ${space(1)} ${space(1.5)};
|
|
|
+ color: ${p => p.theme.subText};
|
|
|
`;
|