Skip to content

Commit 9360692

Browse files
authored
chore(issue-views): Duplicate issue views component to facilitate feature flagging (#84758)
This PR duplicates all Issue Views related components to make it easier to feature flag the upcoming page filters upgrade. All new components have "PF" in their name, and that will be the component updated to support page filters. I cmd+f'd "/issueViews/" in all of the new PF components to ensure that there were no imports from the old issueViews folder. (All of them should import from the new issueViewsPF folder).
1 parent b218c47 commit 9360692

File tree

9 files changed

+3243
-8
lines changed

9 files changed

+3243
-8
lines changed

static/app/components/draggableTabs/draggableTabListPF.tsx

Lines changed: 568 additions & 0 deletions
Large diffs are not rendered by default.

static/app/views/issueList/issueViewsHeaderPF.spec.tsx

Lines changed: 891 additions & 0 deletions
Large diffs are not rendered by default.

static/app/views/issueList/issueViewsHeaderPF.tsx

Lines changed: 456 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {useEffect, useMemo, useRef, useState} from 'react';
2+
import {useTheme} from '@emotion/react';
3+
import styled from '@emotion/styled';
4+
import {motion} from 'framer-motion';
5+
6+
import {GrowingInput} from 'sentry/components/growingInput';
7+
import {Tooltip} from 'sentry/components/tooltip';
8+
9+
interface EditableTabTitlePFProps {
10+
isEditing: boolean;
11+
isSelected: boolean;
12+
label: string;
13+
onChange: (newLabel: string) => void;
14+
setIsEditing: (isEditing: boolean) => void;
15+
}
16+
17+
function EditableTabTitlePF({
18+
label,
19+
onChange,
20+
isEditing,
21+
isSelected,
22+
setIsEditing,
23+
}: EditableTabTitlePFProps) {
24+
const [inputValue, setInputValue] = useState(label);
25+
26+
useEffect(() => {
27+
setInputValue(label);
28+
}, [label]);
29+
30+
const theme = useTheme();
31+
const inputRef = useRef<HTMLInputElement>(null);
32+
const isEmpty = !inputValue.trim();
33+
34+
const memoizedStyles = useMemo(() => {
35+
return {fontWeight: isSelected ? theme.fontWeightBold : theme.fontWeightNormal};
36+
}, [isSelected, theme.fontWeightBold, theme.fontWeightNormal]);
37+
38+
const handleOnBlur = (e: React.FocusEvent<HTMLInputElement, Element>) => {
39+
e.stopPropagation();
40+
e.preventDefault();
41+
const trimmedInputValue = inputValue.trim();
42+
if (!isEditing) {
43+
return;
44+
}
45+
46+
if (isEmpty) {
47+
setInputValue(label);
48+
setIsEditing(false);
49+
return;
50+
}
51+
if (trimmedInputValue !== label) {
52+
onChange(trimmedInputValue);
53+
setInputValue(trimmedInputValue);
54+
}
55+
setIsEditing(false);
56+
};
57+
58+
const handleOnKeyDown = (e: React.KeyboardEvent) => {
59+
if (e.key === 'Enter') {
60+
inputRef.current?.blur();
61+
}
62+
if (e.key === 'Escape') {
63+
setInputValue(label.trim());
64+
setIsEditing(false);
65+
}
66+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === ' ') {
67+
e.stopPropagation();
68+
}
69+
};
70+
71+
useEffect(() => {
72+
if (isEditing) {
73+
requestAnimationFrame(() => {
74+
inputRef.current?.focus();
75+
inputRef.current?.select();
76+
});
77+
} else {
78+
inputRef.current?.blur();
79+
}
80+
}, [isEditing, inputRef]);
81+
82+
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
83+
setInputValue(e.target.value);
84+
};
85+
86+
return (
87+
<Tooltip title={label} disabled={isEditing} showOnlyOnOverflow skipWrapper>
88+
<motion.div layout="position" transition={{duration: 0.2}}>
89+
{isSelected && isEditing ? (
90+
<StyledGrowingInput
91+
value={inputValue}
92+
onChange={handleOnChange}
93+
onKeyDown={handleOnKeyDown}
94+
onBlur={handleOnBlur}
95+
ref={inputRef}
96+
style={memoizedStyles}
97+
isEditing={isEditing}
98+
maxLength={128}
99+
onPointerDown={e => {
100+
e.stopPropagation();
101+
if (!isEditing) {
102+
e.preventDefault();
103+
}
104+
}}
105+
onMouseDown={e => {
106+
e.stopPropagation();
107+
if (!isEditing) {
108+
e.preventDefault();
109+
}
110+
}}
111+
/>
112+
) : (
113+
<UnselectedTabTitle
114+
onDoubleClick={() => setIsEditing(true)}
115+
onPointerDown={e => {
116+
if (isSelected) {
117+
e.stopPropagation();
118+
e.preventDefault();
119+
}
120+
}}
121+
onMouseDown={e => {
122+
if (isSelected) {
123+
e.stopPropagation();
124+
e.preventDefault();
125+
}
126+
}}
127+
isSelected={isSelected}
128+
>
129+
{label}
130+
</UnselectedTabTitle>
131+
)}
132+
</motion.div>
133+
</Tooltip>
134+
);
135+
}
136+
137+
export default EditableTabTitlePF;
138+
139+
const UnselectedTabTitle = styled('div')<{isSelected: boolean}>`
140+
height: 20px;
141+
max-width: ${p => (p.isSelected ? '325px' : '310px')};
142+
white-space: nowrap;
143+
overflow: hidden;
144+
text-overflow: ellipsis;
145+
padding-right: 1px;
146+
cursor: pointer;
147+
line-height: 1.45;
148+
`;
149+
150+
const StyledGrowingInput = styled(GrowingInput)<{
151+
isEditing: boolean;
152+
}>`
153+
position: relative;
154+
border: none;
155+
margin: 0;
156+
padding: 0;
157+
background: transparent;
158+
min-height: 0px;
159+
height: 20px;
160+
border-radius: 0px;
161+
text-overflow: ellipsis;
162+
cursor: text;
163+
max-width: 325px;
164+
line-height: 1.45;
165+
166+
&,
167+
&:focus,
168+
&:active,
169+
&:hover {
170+
box-shadow: none;
171+
}
172+
`;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import styled from '@emotion/styled';
2+
3+
import {Button} from 'sentry/components/button';
4+
import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
5+
import {IconEllipsis, IconMegaphone} from 'sentry/icons';
6+
import {t} from 'sentry/locale';
7+
import {space} from 'sentry/styles/space';
8+
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
9+
10+
interface IssueViewEllipsisMenuPFProps {
11+
menuOptions: MenuItemProps[];
12+
'aria-label'?: string;
13+
hasUnsavedChanges?: boolean;
14+
}
15+
16+
export function IssueViewEllipsisMenuPF({
17+
hasUnsavedChanges = false,
18+
menuOptions,
19+
...props
20+
}: IssueViewEllipsisMenuPFProps) {
21+
return (
22+
<TriggerIconWrap>
23+
<StyledDropdownMenu
24+
position="bottom-start"
25+
triggerProps={{
26+
'aria-label': props['aria-label'] ?? 'Tab Options',
27+
size: 'zero',
28+
showChevron: false,
29+
borderless: true,
30+
icon: (
31+
<ButtonWrapper>
32+
<IconEllipsis compact />
33+
{hasUnsavedChanges && (
34+
<UnsavedChangesIndicator
35+
role="presentation"
36+
data-test-id="unsaved-changes-indicator"
37+
/>
38+
)}
39+
</ButtonWrapper>
40+
),
41+
style: {width: '18px', height: '16px', borderRadius: '4px'},
42+
}}
43+
items={menuOptions}
44+
offset={[-10, 5]}
45+
menuFooter={<FeedbackFooter />}
46+
usePortal
47+
/>
48+
</TriggerIconWrap>
49+
);
50+
}
51+
52+
function FeedbackFooter() {
53+
const openForm = useFeedbackForm();
54+
55+
if (!openForm) {
56+
return null;
57+
}
58+
59+
return (
60+
<SectionedOverlayFooter>
61+
<Button
62+
size="xs"
63+
icon={<IconMegaphone />}
64+
onClick={() =>
65+
openForm({
66+
messagePlaceholder: t('How can we make custom views better for you?'),
67+
tags: {
68+
['feedback.source']: 'custom_views',
69+
['feedback.owner']: 'issues',
70+
},
71+
})
72+
}
73+
>
74+
{t('Give Feedback')}
75+
</Button>
76+
</SectionedOverlayFooter>
77+
);
78+
}
79+
80+
const SectionedOverlayFooter = styled('div')`
81+
grid-area: footer;
82+
display: flex;
83+
align-items: center;
84+
justify-content: center;
85+
padding: ${space(1)};
86+
border-top: 1px solid ${p => p.theme.innerBorder};
87+
`;
88+
89+
const StyledDropdownMenu = styled(DropdownMenu)`
90+
font-weight: ${p => p.theme.fontWeightNormal};
91+
`;
92+
93+
const UnsavedChangesIndicator = styled('div')`
94+
width: 7px;
95+
height: 7px;
96+
border-radius: 50%;
97+
background: ${p => p.theme.active};
98+
border: solid 1px ${p => p.theme.background};
99+
position: absolute;
100+
top: -3px;
101+
right: -3px;
102+
`;
103+
const ButtonWrapper = styled('div')`
104+
width: 18px;
105+
height: 16px;
106+
border: 1px solid ${p => p.theme.gray200};
107+
border-radius: 4px;
108+
`;
109+
110+
const TriggerIconWrap = styled('div')`
111+
position: relative;
112+
display: flex;
113+
align-items: center;
114+
`;

0 commit comments

Comments
 (0)