Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/components/PropertyField/PropertyDropdownField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,26 @@ describe("PropertyDropdownField", () => {

expect(renderOptions).not.toHaveBeenCalled();
});

it("renders disabled options as unavailable", () => {
const markup = renderToStaticMarkup(
React.createElement(PropertyDropdownField, {
value: "open",
label: "Open",
icon: null,
active: true,
searchable: false,
options: [
{ value: "open", label: "Open", disabled: true },
{ value: "closed", label: "Closed" },
],
dataTestId: "status",
})
);

expect(markup).toMatch(
/data-testid="status-option-open"[^>]*disabled=""[^>]*aria-disabled="true"/
);
expect(markup).toContain('data-testid="status-option-closed"');
});
});
2 changes: 2 additions & 0 deletions src/components/PropertyField/PropertyDropdownField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface PropertyDropdownOption<T extends string> {
label: string;
icon?: React.ReactNode;
iconColor?: string;
disabled?: boolean;
}

export type PropertyDropdownPlacement = "inline" | "portal";
Expand Down Expand Up @@ -223,6 +224,7 @@ export function PropertyDropdownField<T extends string>({
iconColor={option.iconColor}
label={option.label}
isSelected={option.value === value}
disabled={option.disabled}
onClick={() => handleSelect(option.value)}
dataTestId={
dataTestId ? `${dataTestId}-option-${option.value}` : undefined
Expand Down
7 changes: 6 additions & 1 deletion src/components/PropertyField/PropertyFieldEditable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ export interface OptionProps {
iconColor?: string;
label: string;
isSelected?: boolean;
disabled?: boolean;
onClick: () => void;
children?: React.ReactNode;
/** Stable selector for rendered UI tests. */
Expand All @@ -358,6 +359,7 @@ export const Option: React.FC<OptionProps> = ({
iconColor,
label,
isSelected,
disabled = false,
onClick,
children,
dataTestId,
Expand All @@ -367,13 +369,16 @@ export const Option: React.FC<OptionProps> = ({
data-testid={dataTestId}
className={[
DROPDOWN_CLASSES.item,
DROPDOWN_CLASSES.itemHover,
!disabled && DROPDOWN_CLASSES.itemHover,
"w-full justify-between text-left",
isSelected && DROPDOWN_CLASSES.itemSelected,
disabled && DROPDOWN_CLASSES.itemDisabled,
]
.filter(Boolean)
.join(" ")}
onClick={onClick}
disabled={disabled}
aria-disabled={disabled}
>
{children ? (
<>
Expand Down
8 changes: 6 additions & 2 deletions src/hooks/ui/layout/useElementDimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface ElementDimensions {
export interface UseElementDimensionsOptions {
/** What to measure: 'width', 'height', or 'both' */
dimension?: DimensionType;
/** Disable measurement and listener ownership while the element is absent. */
enabled?: boolean;
/** Additional dependency to trigger re-measurement */
deps?: unknown[];
}
Expand Down Expand Up @@ -70,14 +72,16 @@ export function useElementDimensions(
ref: RefObject<HTMLElement | null | { current?: HTMLElement | null }>,
options: UseElementDimensionsOptions = {}
): number | ElementDimensions {
const { dimension = "both", deps = [] } = options;
const { dimension = "both", enabled = true, deps = [] } = options;

const [dimensions, setDimensions] = useState<ElementDimensions>({
width: 0,
height: 0,
});

useIsomorphicLayoutEffect(() => {
if (!enabled) return;

const measureDimensions = () => {
// Handle nested ref objects
const element =
Expand Down Expand Up @@ -130,7 +134,7 @@ export function useElementDimensions(
window.removeEventListener("resize", measureDimensions);
resizeObserver?.disconnect();
};
}, [ref, ...deps]);
}, [ref, enabled, ...deps]);

// Return based on requested dimension
if (dimension === "width") return dimensions.width;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ describe("mapGitHubIssueToThreadWorkItem", () => {
`data-testid="work-item-property-status-${issue.html_url}"`
);
expect(markup).toContain('data-testid="github-issue-inline-composer"');
expect(markup).toContain('data-testid="work-item-thread-floating-footer"');
expect(markup).toContain("padding-bottom:240px");
expect(markup).not.toContain(
'data-testid="work-item-thread-secondary-navigation"'
);
Expand All @@ -183,7 +185,7 @@ describe("mapGitHubIssueToThreadWorkItem", () => {
);
expect(
markup.match(/data-scroll-trail-target/g)?.length
).toBeGreaterThanOrEqual(4);
).toBeGreaterThanOrEqual(3);
});

it("toggles external assignees without duplicating login casing", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";

import type {
GitHubIssue,
Expand Down Expand Up @@ -73,6 +74,7 @@ const GitHubIssueThreadSurface: React.FC<GitHubIssueThreadSurfaceProps> = ({
interaction,
assigneeConfig,
}) => {
const { t } = useTranslation("common");
const workItem = useMemo(
() => mapGitHubIssueToThreadWorkItem(issue),
[issue]
Expand All @@ -99,8 +101,16 @@ const GitHubIssueThreadSurface: React.FC<GitHubIssueThreadSurfaceProps> = ({
externalStatusConfig: {
currentStatusId: issue.state,
options: [
{ id: "open", label: "Open" },
{ id: "closed", label: "Closed" },
{
id: "open",
label: t("git.issues.status.open"),
color: "var(--color-success-6)",
},
{
id: "closed",
label: t("git.issues.status.closed"),
color: "var(--color-purple-6)",
},
],
disabled: !interaction.canManageStatus || interaction.updatingStatus,
onChangeStatusId: (statusId) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CircleDot,
CircleSlash,
Copy,
Loader2,
} from "lucide-react";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
Expand All @@ -17,7 +18,6 @@ import {
DROPDOWN_ITEM,
DROPDOWN_WIDTHS,
} from "@src/components/Dropdown/tokens";
import { LoadingBar } from "@src/modules/shared/layouts/blocks";

import type {
GitHubIssueInteractionConfig,
Expand Down Expand Up @@ -106,8 +106,16 @@ const GitHubIssueCloseButton: React.FC<GitHubIssueCloseButtonProps> = ({
autoFocus
/>
{interaction.loadingDuplicateCandidates ? (
<div className="px-2 py-3">
<LoadingBar />
<div
className={DROPDOWN_CLASSES.listMessage}
data-testid="github-issue-duplicate-loading"
>
<Loader2
size={DROPDOWN_ITEM.iconSize}
className="animate-spin"
aria-hidden
/>
<span>{t("actions.loading")}</span>
</div>
) : interaction.duplicateCandidatesError ? (
<div className={DROPDOWN_CLASSES.listMessage} role="status">
Expand Down Expand Up @@ -162,6 +170,8 @@ const GitHubIssueCloseButton: React.FC<GitHubIssueCloseButtonProps> = ({
<DropdownItem
icon={<CircleDot size={DROPDOWN_ITEM.iconSize} aria-hidden />}
onClick={closeMenu}
disabled={interaction.issueState === "open"}
dataTestId="github-issue-status-open"
>
{t("git.issues.status.open")}
</DropdownItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,25 @@ const GitHubIssueComposer: React.FC<GitHubIssueComposerProps> = ({
<section
data-testid="github-issue-inline-composer"
aria-label={t("git.issues.composer.addComment")}
className="flex flex-col gap-2"
>
<ComposerShell variant="default" className="!gap-0 overflow-visible !p-0">
{interaction.canManageStatus ? (
<div
className="flex min-h-9 flex-wrap items-center gap-2 px-1"
data-testid="github-issue-level-actions"
>
<GitHubIssueCloseButton
interaction={interaction}
onStatusChange={handleStatusChange}
/>
</div>
) : null}

<ComposerShell
variant="default"
className="!gap-0 overflow-visible !p-0"
data-testid="github-issue-comment-input"
>
<RichMarkdownEditor
value={commentBody}
onChange={(markdown) => setCommentBody(markdown)}
Expand All @@ -84,6 +101,8 @@ const GitHubIssueComposer: React.FC<GitHubIssueComposerProps> = ({
maxHeight={500}
appearance="plain"
toolbarMode="inline"
toolbarSize="mini"
toolbarDropdownPosition="top-start"
editable={interaction.canComment && !interaction.submittingComment}
dataTestId="github-issue-comment-editor"
/>
Expand Down Expand Up @@ -118,26 +137,18 @@ const GitHubIssueComposer: React.FC<GitHubIssueComposerProps> = ({
</span>
)}

<div className="flex flex-wrap items-center justify-end gap-2">
{interaction.canManageStatus ? (
<GitHubIssueCloseButton
interaction={interaction}
onStatusChange={handleStatusChange}
/>
) : null}
<Button
htmlType="button"
variant="primary"
size="default"
shape="round"
loading={interaction.submittingComment}
disabled={!hasComment || !interaction.canComment}
onClick={() => void handleComment()}
data-testid="github-issue-comment-submit"
>
{t("git.issues.composer.submitComment")}
</Button>
</div>
<Button
htmlType="button"
variant="primary"
size="default"
shape="round"
loading={interaction.submittingComment}
disabled={!hasComment || !interaction.canComment}
onClick={() => void handleComment()}
data-testid="github-issue-comment-submit"
>
{t("git.issues.composer.submitComment")}
</Button>
</div>
</ComposerShell>
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ const HistoryTab: React.FC<HistoryTabProps> = ({
maxHeight={120}
appearance="plain"
matchMarkdownPreview={false}
toolbarSize="mini"
toolbarDropdownPosition="top-start"
dataTestId="work-item-comment-editor"
/>
{submitButton}
Expand All @@ -150,6 +152,8 @@ const HistoryTab: React.FC<HistoryTabProps> = ({
minHeight={60}
maxHeight={120}
appearance="outlined"
toolbarSize="mini"
toolbarDropdownPosition="top-start"
dataTestId="work-item-comment-editor"
/>
<div className="mt-2 flex items-center justify-end">{submitButton}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ describe("GitHubIssueComposer", () => {
);
expect(editor?.dataset.toolbarMode).toBe("inline");
expect(editor?.dataset.minHeight).toBe("140");
const levelActions = container.querySelector(
"[data-testid='github-issue-level-actions']"
);
const input = container.querySelector(
"[data-testid='github-issue-comment-input']"
);
expect(levelActions?.nextElementSibling).toBe(input);
expect(input?.contains(levelActions as Node)).toBe(false);
expect(levelActions?.className).not.toContain("border-");
expect(
input?.querySelector("[data-testid='github-issue-comment-submit']")
).not.toBeNull();
expect(
levelActions?.querySelector(
"[data-testid='github-issue-comment-status-action']"
)
).not.toBeNull();
act(() => {
const valueSetter = Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
Expand Down Expand Up @@ -201,6 +218,11 @@ describe("GitHubIssueComposer", () => {
expect(
document.querySelector("[data-testid='github-issue-close-menu']")
).not.toBeNull();
expect(
document
.querySelector("[data-testid='github-issue-status-open']")
?.getAttribute("aria-disabled")
).toBe("true");
expect(config.onLoadDuplicateCandidates).not.toHaveBeenCalled();

await act(async () => {
Expand All @@ -217,6 +239,34 @@ describe("GitHubIssueComposer", () => {
).not.toBeNull();
});

it("uses an inline spinner while duplicate issues load", async () => {
const config = interaction({ loadingDuplicateCandidates: true });
act(() => {
root.render(createElement(GitHubIssueComposer, { interaction: config }));
});

await act(async () => {
container
.querySelectorAll<HTMLButtonElement>(".button-split-wrapper button")[1]
?.click();
await Promise.resolve();
});
act(() => {
document
.querySelector<HTMLElement>(
"[data-testid='github-issue-close-duplicate']"
)
?.click();
});

const loading = document.querySelector(
"[data-testid='github-issue-duplicate-loading']"
);
expect(loading?.querySelector(".animate-spin")).not.toBeNull();
expect(loading?.textContent).toContain("actions.loading");
expect(config.onLoadDuplicateCandidates).not.toHaveBeenCalled();
});

it("closes as a duplicate with the selected canonical issue database ID", async () => {
const canonicalIssue = {
id: 100_987,
Expand Down
Loading
Loading