Skip to content

Commit 87e5fd7

Browse files
author
KennyG
committed
Updated LinkButton to combine mismatch warning hover
- Converted OperationButton to use React.forwardRef for better ref handling. - Enhanced LinkButton to display a popover warning for performer collisions, improving user feedback. - Cleaned up styles and removed unused CSS classes in Tagger components for better maintainability.
1 parent 4701a45 commit 87e5fd7

5 files changed

Lines changed: 162 additions & 141 deletions

File tree

ui/v2.5/src/components/Shared/OperationButton.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ interface IOperationButton extends ButtonProps {
99
setLoading?: (v: boolean) => void;
1010
}
1111

12-
export const OperationButton: React.FC<IOperationButton> = (props) => {
12+
export const OperationButton = React.forwardRef<
13+
HTMLButtonElement,
14+
IOperationButton
15+
>(function OperationButton(props, ref) {
1316
const [internalLoading, setInternalLoading] = useState(false);
1417
const mounted = useRef(false);
1518

@@ -44,7 +47,7 @@ export const OperationButton: React.FC<IOperationButton> = (props) => {
4447
}
4548

4649
return (
47-
<Button onClick={handleClick} {...withoutExtras}>
50+
<Button ref={ref} onClick={handleClick} {...withoutExtras}>
4851
{loading && (
4952
<span className="mr-2">
5053
<LoadingIndicator message="" inline small />
@@ -53,4 +56,4 @@ export const OperationButton: React.FC<IOperationButton> = (props) => {
5356
{(!loading || !hideChildrenWhenLoading) && props.children}
5457
</Button>
5558
);
56-
};
59+
});
Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,105 @@
1-
import React from "react";
1+
import React, { useCallback, useEffect, useRef, useState } from "react";
22
import { useIntl } from "react-intl";
3-
import { faLink } from "@fortawesome/free-solid-svg-icons";
3+
import { Overlay, Popover } from "react-bootstrap";
4+
import { faLink, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
45

56
import { OperationButton } from "../Shared/OperationButton";
67
import { Icon } from "../Shared/Icon";
8+
import { PerformerCollisionPopoverContent } from "./scenes/TaggerPerformerPopover";
9+
10+
const ENTER_DELAY = 200;
11+
const LEAVE_DELAY = 200;
712

813
export const LinkButton: React.FC<{
914
disabled: boolean;
1015
onLink: () => Promise<void>;
11-
}> = ({ disabled, onLink }) => {
16+
collisionMessageIds?: string[];
17+
}> = ({ disabled, onLink, collisionMessageIds = [] }) => {
1218
const intl = useIntl();
19+
const buttonRef = useRef<HTMLButtonElement>(null);
20+
const [showPopover, setShowPopover] = useState(false);
21+
const enterTimer = useRef<number>();
22+
const leaveTimer = useRef<number>();
23+
const popoverId = useRef(
24+
`link-button-collision-${Math.random().toString(36).slice(2)}`
25+
);
26+
27+
const hasCollision = collisionMessageIds.length > 0;
28+
29+
const handleMouseEnter = useCallback(() => {
30+
if (!hasCollision) return;
31+
32+
window.clearTimeout(leaveTimer.current);
33+
enterTimer.current = window.setTimeout(
34+
() => setShowPopover(true),
35+
ENTER_DELAY
36+
);
37+
}, [hasCollision]);
38+
39+
const handleMouseLeave = useCallback(() => {
40+
if (!hasCollision) return;
41+
42+
window.clearTimeout(enterTimer.current);
43+
leaveTimer.current = window.setTimeout(
44+
() => setShowPopover(false),
45+
LEAVE_DELAY
46+
);
47+
}, [hasCollision]);
48+
49+
useEffect(
50+
() => () => {
51+
window.clearTimeout(enterTimer.current);
52+
window.clearTimeout(leaveTimer.current);
53+
},
54+
[]
55+
);
1356

1457
return (
15-
<OperationButton
16-
variant="secondary"
17-
disabled={disabled}
18-
operation={onLink}
19-
hideChildrenWhenLoading
20-
title={intl.formatMessage({ id: "component_tagger.verb_link_existing" })}
21-
>
22-
<Icon icon={faLink} />
23-
</OperationButton>
58+
<>
59+
<OperationButton
60+
ref={buttonRef}
61+
variant="secondary"
62+
disabled={disabled}
63+
operation={onLink}
64+
hideChildrenWhenLoading
65+
title={intl.formatMessage({ id: "component_tagger.verb_link_existing" })}
66+
onMouseEnter={handleMouseEnter}
67+
onMouseLeave={handleMouseLeave}
68+
>
69+
<Icon
70+
icon={hasCollision ? faTriangleExclamation : faLink}
71+
className={hasCollision ? "text-warning" : undefined}
72+
/>
73+
</OperationButton>
74+
{hasCollision && (
75+
<Overlay
76+
show={showPopover}
77+
placement="bottom-end"
78+
target={buttonRef}
79+
container={document.body}
80+
popperConfig={{
81+
modifiers: [
82+
{
83+
name: "offset",
84+
options: {
85+
offset: [0, 6],
86+
},
87+
},
88+
],
89+
}}
90+
>
91+
<Popover
92+
id={popoverId.current}
93+
className="hover-popover-content"
94+
onMouseEnter={handleMouseEnter}
95+
onMouseLeave={handleMouseLeave}
96+
>
97+
<PerformerCollisionPopoverContent
98+
messageIds={collisionMessageIds}
99+
/>
100+
</Popover>
101+
</Overlay>
102+
)}
103+
</>
24104
);
25105
};

ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useMemo, useRef, useState } from "react";
1+
import React, { useEffect, useMemo, useState } from "react";
22
import { Button, ButtonGroup } from "react-bootstrap";
33
import { FormattedMessage } from "react-intl";
44

@@ -13,7 +13,7 @@ import { ExternalLink } from "src/components/Shared/ExternalLink";
1313
import { Link } from "react-router-dom";
1414
import { LinkButton } from "../LinkButton";
1515
import {
16-
PerformerCollisionWarning,
16+
getPerformerCollisionMessageIds,
1717
TaggerPerformerPopover,
1818
} from "./TaggerPerformerPopover";
1919

@@ -78,7 +78,6 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
7878
stashID.stash_id === performer.remote_site_id
7979
);
8080
const [selectedPerformer, setSelectedPerformer] = useState<Performer>();
81-
const controlsRef = useRef<HTMLDivElement>(null);
8281

8382
const { data: selectedPerformerData } = GQL.useFindPerformerQuery({
8483
variables: { id: selectedID ?? "" },
@@ -162,6 +161,13 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
162161
}
163162

164163
const selectedSource = !selectedID ? "skip" : "existing";
164+
const collisionMessageIds = selectedLocalPerformer
165+
? getPerformerCollisionMessageIds(
166+
performer,
167+
selectedLocalPerformer,
168+
endpoint
169+
)
170+
: [];
165171

166172
const safeBuildPerformerScraperLink = (id: string | null | undefined) => {
167173
return stashboxPerformerPrefix && id
@@ -185,52 +191,46 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
185191
</TaggerPerformerPopover>
186192
</b>
187193
</div>
188-
<div className="performer-result-controls" ref={controlsRef}>
189-
<ButtonGroup>
190-
<Button variant="secondary" onClick={() => onCreate()}>
191-
<FormattedMessage id="actions.create" />
192-
</Button>
193-
<Button
194-
variant={selectedSource === "skip" ? "primary" : "secondary"}
195-
onClick={() => handleSkip()}
196-
>
197-
<FormattedMessage id="actions.skip" />
198-
</Button>
199-
<PerformerSelect
200-
values={selectedPerformer ? [selectedPerformer] : []}
201-
onSelect={handleSelect}
202-
active={selectedSource === "existing"}
203-
isClearable={false}
204-
ageFromDate={ageFromDate}
205-
wrapValueContainer={(container, selected) => (
206-
<TaggerPerformerPopover
207-
performer={
208-
selectedLocalPerformer?.id === selected.id
209-
? selectedLocalPerformer
210-
: undefined
211-
}
212-
performerID={selected.id}
213-
scrapedPerformer={performer}
214-
endpoint={endpoint}
215-
triggerClassName="performer-select-value-hover-trigger"
216-
>
217-
{container}
218-
</TaggerPerformerPopover>
219-
)}
220-
/>
221-
{endpoint && onLink && (
222-
<LinkButton disabled={selectedID === undefined} onLink={onLink} />
194+
<ButtonGroup>
195+
<Button variant="secondary" onClick={() => onCreate()}>
196+
<FormattedMessage id="actions.create" />
197+
</Button>
198+
<Button
199+
variant={selectedSource === "skip" ? "primary" : "secondary"}
200+
onClick={() => handleSkip()}
201+
>
202+
<FormattedMessage id="actions.skip" />
203+
</Button>
204+
<PerformerSelect
205+
values={selectedPerformer ? [selectedPerformer] : []}
206+
onSelect={handleSelect}
207+
active={selectedSource === "existing"}
208+
isClearable={false}
209+
ageFromDate={ageFromDate}
210+
wrapValueContainer={(container, selected) => (
211+
<TaggerPerformerPopover
212+
performer={
213+
selectedLocalPerformer?.id === selected.id
214+
? selectedLocalPerformer
215+
: undefined
216+
}
217+
performerID={selected.id}
218+
scrapedPerformer={performer}
219+
endpoint={endpoint}
220+
triggerClassName="performer-select-value-hover-trigger"
221+
>
222+
{container}
223+
</TaggerPerformerPopover>
223224
)}
224-
</ButtonGroup>
225-
{selectedLocalPerformer && (
226-
<PerformerCollisionWarning
227-
scrapedPerformer={performer}
228-
localPerformer={selectedLocalPerformer}
229-
endpoint={endpoint}
230-
anchorRef={controlsRef}
225+
/>
226+
{endpoint && onLink && (
227+
<LinkButton
228+
disabled={selectedID === undefined}
229+
onLink={onLink}
230+
collisionMessageIds={collisionMessageIds}
231231
/>
232232
)}
233-
</div>
233+
</ButtonGroup>
234234
</div>
235235
);
236236
};

ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx

Lines changed: 18 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import React, { useState } from "react";
22
import * as GQL from "src/core/generated-graphql";
3-
import { OverlayProps } from "react-bootstrap";
43
import { Placement } from "react-bootstrap/esm/Overlay";
54
import { FormattedMessage } from "react-intl";
65
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
76
import { PerformerPopover } from "src/components/Performers/PerformerPopover";
87
import { PerformerCard } from "src/components/Performers/PerformerCard";
98
import { Icon } from "src/components/Shared/Icon";
10-
import {
11-
PopoverCard,
12-
WarningHoverPopover,
13-
} from "src/components/Shared/HoverPopover";
9+
import { PopoverCard } from "src/components/Shared/HoverPopover";
1410
import { ScrapedPerformerCard } from "./ScrapedPerformerCard";
1511

1612
const normalizeValue = (value: unknown) =>
@@ -88,71 +84,23 @@ export const hasPerformerCollision = (
8884
endpoint?: string
8985
) => getPerformerCollisionMessageIds(remote, local, endpoint).length > 0;
9086

91-
const performerCollisionPopperConfig: OverlayProps["popperConfig"] = {
92-
modifiers: [
93-
{
94-
name: "offset",
95-
options: {
96-
offset: [0, 6],
97-
},
98-
},
99-
{
100-
name: "sameWidth",
101-
enabled: true,
102-
phase: "beforeWrite",
103-
requires: ["computeStyles"],
104-
fn({ state }) {
105-
state.styles.popper.width = `${state.rects.reference.width}px`;
106-
},
107-
},
108-
],
109-
};
110-
111-
interface IPerformerCollisionWarningProps {
112-
scrapedPerformer: GQL.ScrapedPerformer;
113-
localPerformer: GQL.PerformerDataFragment;
114-
endpoint?: string;
115-
anchorRef: React.RefObject<HTMLElement>;
116-
}
117-
118-
export const PerformerCollisionWarning: React.FC<
119-
IPerformerCollisionWarningProps
120-
> = ({ scrapedPerformer, localPerformer, endpoint, anchorRef }) => {
121-
const messageIds = getPerformerCollisionMessageIds(
122-
scrapedPerformer,
123-
localPerformer,
124-
endpoint
125-
);
126-
127-
if (messageIds.length === 0) {
128-
return null;
129-
}
130-
131-
return (
132-
<span className="SceneTaggerIcon performer-collision-warning-trigger">
133-
<WarningHoverPopover
134-
placement="bottom-end"
135-
target={anchorRef}
136-
popperConfig={performerCollisionPopperConfig}
137-
content={
138-
<PopoverCard className="performer-collision-popover">
139-
<div className="performer-collision-warnings">
140-
{messageIds.map((messageId) => (
141-
<div key={messageId} className="performer-collision-warning">
142-
<Icon
143-
className="text-warning performer-collision-warning-icon"
144-
icon={faTriangleExclamation}
145-
/>
146-
<FormattedMessage id={messageId} />
147-
</div>
148-
))}
149-
</div>
150-
</PopoverCard>
151-
}
152-
/>
153-
</span>
154-
);
155-
};
87+
export const PerformerCollisionPopoverContent: React.FC<{
88+
messageIds: string[];
89+
}> = ({ messageIds }) => (
90+
<PopoverCard className="performer-collision-popover">
91+
<div className="performer-collision-warnings">
92+
{messageIds.map((messageId) => (
93+
<div key={messageId} className="performer-collision-warning">
94+
<Icon
95+
className="text-warning performer-collision-warning-icon"
96+
icon={faTriangleExclamation}
97+
/>
98+
<FormattedMessage id={messageId} />
99+
</div>
100+
))}
101+
</div>
102+
</PopoverCard>
103+
);
156104

157105
interface ITaggerPerformerPopoverProps {
158106
performer?: GQL.PerformerDataFragment;

ui/v2.5/src/components/Tagger/styles.scss

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,6 @@
101101
margin-right: 10px;
102102
width: var(--fa-fw-width, 1.25em);
103103
}
104-
105-
.performer-result-controls {
106-
align-items: center;
107-
display: inline-flex;
108-
flex-shrink: 0;
109-
}
110-
111-
.performer-collision-warning-trigger {
112-
align-self: center;
113-
}
114104
}
115105

116106
.performer-collision-popover {

0 commit comments

Comments
 (0)