Skip to content

Commit 358ce11

Browse files
authored
feat(platform): preview project name on top of DSN (#13041)
* feat(platform): preview project name on top of DSN also fixes a "copied" feedback z-index issue closes #13015
1 parent 7bef58c commit 358ce11

File tree

7 files changed

+99
-11
lines changed

7 files changed

+99
-11
lines changed

src/components/codeBlock/code-blocks.module.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,5 @@
150150
border: none;
151151
color: var(--white);
152152
transition: opacity 150ms;
153+
z-index: 10000;
153154
}

src/components/codeBlock/index.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,58 @@ export interface CodeBlockProps {
1515
title?: string;
1616
}
1717

18+
/**
19+
*
20+
* Copy `element`'s text children as long as long as they are not `.no-copy`
21+
*/
22+
function getCopiableText(element: HTMLDivElement) {
23+
let text = '';
24+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
25+
acceptNode: function (node) {
26+
// Skip if parent has .no-copy class
27+
if (node.parentElement?.classList.contains('no-copy')) {
28+
return NodeFilter.FILTER_REJECT;
29+
}
30+
return NodeFilter.FILTER_ACCEPT;
31+
},
32+
});
33+
34+
let node: Node | null;
35+
// eslint-disable-next-line no-cond-assign
36+
while ((node = walker.nextNode())) {
37+
text += node.textContent;
38+
}
39+
40+
return text.trim();
41+
}
42+
1843
export function CodeBlock({filename, language, children}: CodeBlockProps) {
1944
const [showCopied, setShowCopied] = useState(false);
2045
const codeRef = useRef<HTMLDivElement>(null);
2146

2247
// Show the copy button after js has loaded
2348
// otherwise the copy button will not work
2449
const [showCopyButton, setShowCopyButton] = useState(false);
50+
2551
useEffect(() => {
2652
setShowCopyButton(true);
53+
// prevent .no-copy elements from being copied during selection Right click copy or / Cmd+C
54+
const noCopyElements = codeRef.current?.querySelectorAll<HTMLSpanElement>('.no-copy');
55+
const handleSelectionChange = () => {
56+
// hide no copy elements within the selection
57+
const selection = window.getSelection();
58+
noCopyElements?.forEach(element => {
59+
if (selection?.containsNode(element, true)) {
60+
element.style.display = 'none';
61+
} else {
62+
element.style.display = 'inline';
63+
}
64+
});
65+
};
66+
document.addEventListener('selectionchange', handleSelectionChange);
67+
return () => {
68+
document.removeEventListener('selectionchange', handleSelectionChange);
69+
};
2770
}, []);
2871

2972
useCleanSnippetInClipboard(codeRef, {language});
@@ -33,7 +76,9 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) {
3376
return;
3477
}
3578

36-
const code = cleanCodeSnippet(codeRef.current.innerText, {language});
79+
const code = cleanCodeSnippet(getCopiableText(codeRef.current), {
80+
language,
81+
});
3782

3883
try {
3984
await navigator.clipboard.writeText(code);

src/components/codeKeywords/codeKeywords.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export function makeKeywordsClickable(children: React.ReactNode) {
2727
if (ORG_AUTH_TOKEN_REGEX.test(child)) {
2828
makeOrgAuthTokenClickable(arr, child);
2929
} else if (KEYWORDS_REGEX.test(child)) {
30-
makeProjectKeywordsClickable(arr, child);
30+
const isDSNKeyword = /___PUBLIC_DSN___/.test(child);
31+
makeProjectKeywordsClickable(arr, child, isDSNKeyword);
3132
} else {
3233
arr.push(child);
3334
}
@@ -42,13 +43,18 @@ function makeOrgAuthTokenClickable(arr: ChildrenItem[], str: string) {
4243
));
4344
}
4445

45-
function makeProjectKeywordsClickable(arr: ChildrenItem[], str: string) {
46+
function makeProjectKeywordsClickable(
47+
arr: ChildrenItem[],
48+
str: string,
49+
isDSNKeyword = false
50+
) {
4651
runRegex(arr, str, KEYWORDS_REGEX, (lastIndex, match) => (
4752
<KeywordSelector
4853
key={`project-keyword-${lastIndex}`}
4954
index={lastIndex}
5055
group={match[1] || 'PROJECT'}
5156
keyword={match[2]}
57+
showPreview={isDSNKeyword}
5258
/>
5359
));
5460
}

src/components/codeKeywords/keyword.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import {MotionProps} from 'framer-motion';
44

5-
import {KeywordSpan} from './styles.css';
5+
import {KeywordSpan} from './styles';
66

77
export function Keyword({
88
initial = {opacity: 0, y: -10, position: 'absolute'},
@@ -17,14 +17,16 @@ export function Keyword({
1717
opacity: {duration: 0.15},
1818
y: {duration: 0.25},
1919
},
20+
showPreview: hasPreview = false,
2021
...props
21-
}: MotionProps) {
22+
}: MotionProps & {showPreview?: boolean}) {
2223
return (
2324
<KeywordSpan
2425
initial={initial}
2526
animate={animate}
2627
exit={exit}
2728
transition={transition}
29+
hasPreview={hasPreview}
2830
{...props}
2931
/>
3032
);

src/components/codeKeywords/keywordSelector.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,24 @@ import {
2121
KeywordIndicator,
2222
KeywordSearchInput,
2323
PositionWrapper,
24+
ProjectPreview,
2425
Selections,
25-
} from './styles.css';
26+
} from './styles';
2627
import {dropdownPopperOptions} from './utils';
2728

2829
type KeywordSelectorProps = {
2930
group: string;
3031
index: number;
3132
keyword: string;
33+
showPreview: boolean;
3234
};
3335

34-
export function KeywordSelector({keyword, group, index}: KeywordSelectorProps) {
36+
export function KeywordSelector({
37+
keyword,
38+
group,
39+
index,
40+
showPreview,
41+
}: KeywordSelectorProps) {
3542
const [isOpen, setIsOpen] = useState(false);
3643
const [referenceEl, setReferenceEl] = useState<HTMLSpanElement | null>(null);
3744
const [dropdownEl, setDropdownEl] = useState<HTMLElement | null>(null);
@@ -137,17 +144,22 @@ export function KeywordSelector({keyword, group, index}: KeywordSelectorProps) {
137144
// correctly overlap during animations, but this must be removed
138145
// after so copy-paste correctly works.
139146
display: isAnimating ? 'inline-grid' : undefined,
147+
position: 'relative',
140148
}}
141149
>
142150
<AnimatePresence initial={false}>
143151
<Keyword
144152
onAnimationStart={() => setIsAnimating(true)}
145153
onAnimationComplete={() => setIsAnimating(false)}
146154
key={currentSelectionIdx}
155+
showPreview={showPreview}
147156
>
148157
{currentSelection[keyword]}
149158
</Keyword>
150159
</AnimatePresence>
160+
{!isOpen && showPreview && currentSelection?.title && (
161+
<ProjectPreview className="no-copy">{currentSelection.title}</ProjectPreview>
162+
)}
151163
</span>
152164
</KeywordDropdown>
153165
{isMounted &&

src/components/codeKeywords/orgAuthTokenCreator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
KeywordDropdown,
2222
PositionWrapper,
2323
Selections,
24-
} from './styles.css';
24+
} from './styles';
2525
import {dropdownPopperOptions} from './utils';
2626

2727
type TokenState =

src/components/codeKeywords/styles.css.ts renamed to src/components/codeKeywords/styles.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ import {ArrowDown} from 'react-feather';
44
import styled from '@emotion/styled';
55
import {motion} from 'framer-motion';
66

7+
export const ProjectPreview = styled('div')`
8+
position: absolute;
9+
top: -24px;
10+
left: 50%;
11+
transform: translateX(-50%);
12+
font-size: 12px;
13+
background-color: rgba(51, 51, 51, 1);
14+
color: #fff;
15+
padding: 2px 6px;
16+
border-radius: 3px;
17+
pointer-events: none;
18+
white-space: nowrap;
19+
opacity: 0.9;
20+
user-select: none;
21+
`;
22+
723
export const PositionWrapper = styled('div')`
824
z-index: 100;
925
`;
@@ -92,8 +108,8 @@ export const ItemButton = styled('button')<{dark: boolean; isActive: boolean}>`
92108
color: #EBE6EF;
93109
`
94110
: `
95-
96-
111+
112+
97113
&:focus {
98114
outline: none;
99115
background-color: ${p.dark ? 'var(--gray-a4)' : 'var(--accent-purple-light)'};
@@ -138,9 +154,15 @@ export const KeywordIndicator = styled(ArrowDown, {
138154
top: -1px;
139155
`;
140156

141-
export const KeywordSpan = styled(motion.span)`
157+
export const KeywordSpan = styled(motion.span, {
158+
shouldForwardProp: prop => prop !== 'hasPreview',
159+
})<{
160+
hasPreview?: boolean;
161+
}>`
142162
grid-row: 1;
143163
grid-column: 1;
164+
display: inline-block;
165+
margin-top: ${p => (p.hasPreview ? '24px' : '0')};
144166
`;
145167

146168
export const KeywordSearchInput = styled('input')<{dark: boolean}>`

0 commit comments

Comments
 (0)