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
16 changes: 9 additions & 7 deletions ui/src/components/layout/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2912,8 +2912,7 @@ function WorkspaceViewsHeader() {
}>();
const navigate = useNavigate();
const [viewDropdownOpen, setViewDropdownOpen] = useState<string | null>(null);
const [filtersDropdownOpen, setFiltersDropdownOpen] = useState<string | null>(null);
const [displayDropdownOpen, setDisplayDropdownOpen] = useState<string | null>(null);
const [toolbarDropdownOpen, setToolbarDropdownOpen] = useState<string | null>(null);
const [createViewModalOpen, setCreateViewModalOpen] = useState(false);
const [viewSearch, setViewSearch] = useState('');
const [customViews, setCustomViews] = useState<IssueViewApiResponse[]>([]);
Expand Down Expand Up @@ -3017,17 +3016,20 @@ function WorkspaceViewsHeader() {
</div>
<div className="flex items-center gap-1">
<WorkspaceViewsFiltersDropdown
openId={filtersDropdownOpen}
onOpen={setFiltersDropdownOpen}
openId={toolbarDropdownOpen}
onOpen={setToolbarDropdownOpen}
/>
<WorkspaceViewsDisplayDropdown
openId={displayDropdownOpen}
onOpen={setDisplayDropdownOpen}
openId={toolbarDropdownOpen}
onOpen={setToolbarDropdownOpen}
/>
<Button
size="sm"
className="gap-1.5 text-[13px] font-medium"
onClick={() => setCreateViewModalOpen(true)}
onClick={() => {
setToolbarDropdownOpen(null);
setCreateViewModalOpen(true);
}}
>
<IconPlus /> Add view
</Button>
Expand Down
41 changes: 34 additions & 7 deletions ui/src/components/work-item/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

const DROPDOWN_Z_INDEX = 9999;
// Must be above modal root (`z-10050`) so dropdowns work inside modals.
const DROPDOWN_Z_INDEX = 10100;
const VIEWPORT_PADDING = 8;
const PANEL_GAP = 4;

export interface DropdownProps {
id: string;
Expand Down Expand Up @@ -49,6 +52,7 @@ export function Dropdown({
top: number;
left?: number;
right?: number;
maxHeight?: number;
} | null>(null);
const open = openId === id;

Expand All @@ -63,14 +67,36 @@ export function Dropdown({
setPosition(null);
return;
}
const rect = triggerRef.current.getBoundingClientRect();
const triggerRect = triggerRef.current.getBoundingClientRect();
const panelRect = panelRef.current?.getBoundingClientRect();
const panelHeight = panelRect?.height ?? 0;
const panelWidth = panelRect?.width ?? 0;
Comment on lines +70 to +73

const availableBelow = window.innerHeight - triggerRect.bottom - VIEWPORT_PADDING - PANEL_GAP;
const availableAbove = triggerRect.top - VIEWPORT_PADDING - PANEL_GAP;

// Prefer opening below, but flip above when below space is tighter.
let top = triggerRect.bottom + PANEL_GAP;
if (panelHeight > 0 && availableBelow < panelHeight && availableAbove > availableBelow) {
top = Math.max(VIEWPORT_PADDING, triggerRect.top - panelHeight - PANEL_GAP);
}

// Always constrain panel within viewport and allow internal scrolling.
const maxHeight = Math.max(120, Math.max(availableBelow, availableAbove));

if (align === 'right') {
setPosition({
top: rect.bottom + 4,
right: window.innerWidth - rect.right,
});
const unclampedRight = window.innerWidth - triggerRect.right;
const maxRight = Math.max(
VIEWPORT_PADDING,
window.innerWidth - panelWidth - VIEWPORT_PADDING,
);
const right = Math.min(Math.max(unclampedRight, VIEWPORT_PADDING), maxRight);
setPosition({ top, right, maxHeight });
} else {
setPosition({ top: rect.bottom + 4, left: rect.left });
const unclampedLeft = triggerRect.left;
const maxLeft = Math.max(VIEWPORT_PADDING, window.innerWidth - panelWidth - VIEWPORT_PADDING);
const left = Math.min(Math.max(unclampedLeft, VIEWPORT_PADDING), maxLeft);
setPosition({ top, left, maxHeight });
}
}, [open, align]);

Expand Down Expand Up @@ -122,6 +148,7 @@ export function Dropdown({
top: position.top,
...(position.left !== undefined && { left: position.left }),
...(position.right !== undefined && { right: position.right }),
...(position.maxHeight !== undefined && { maxHeight: position.maxHeight }),
zIndex: DROPDOWN_Z_INDEX,
}}
>
Expand Down
7 changes: 5 additions & 2 deletions ui/src/components/workspace-views/CreateViewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ export function CreateViewModal({ open, onClose, onCreated }: CreateViewModalPro

useEffect(() => {
if (open) {
setOpenPanel(null);
setLocalFilters(contextFilters);
setLocalDisplay(contextDisplay);
} else {
setOpenPanel(null);
}
}, [open, contextFilters, contextDisplay]);

Expand Down Expand Up @@ -125,7 +128,7 @@ export function CreateViewModal({ open, onClose, onCreated }: CreateViewModalPro
displayValue=""
triggerContent={<span>Filters</span>}
triggerClassName="inline-flex items-center justify-center rounded-md border border-(--border-subtle) bg-(--bg-surface-1) px-3 py-2 text-sm font-medium text-(--txt-primary) hover:bg-(--bg-layer-1-hover)"
panelClassName="flex w-[280px] max-h-[min(70vh,28rem)] flex-col rounded-md border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised) overflow-hidden"
panelClassName="flex w-[min(280px,calc(100vw-2rem))] max-h-[min(52vh,22rem)] flex-col overflow-hidden rounded-md border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)"
align="left"
>
{workspaceSlug && (
Expand All @@ -149,7 +152,7 @@ export function CreateViewModal({ open, onClose, onCreated }: CreateViewModalPro
displayValue=""
triggerContent={<span>Display</span>}
triggerClassName="inline-flex items-center justify-center rounded-md border border-(--border-subtle) bg-(--bg-surface-1) px-3 py-2 text-sm font-medium text-(--txt-primary) hover:bg-(--bg-layer-1-hover)"
panelClassName="flex min-w-[280px] max-w-[320px] flex-col rounded-md border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised) overflow-hidden"
panelClassName="flex w-[min(320px,calc(100vw-2rem))] max-h-[min(52vh,22rem)] flex-col overflow-hidden rounded-md border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)"
align="left"
>
<WorkspaceViewsDisplayPanel display={localDisplay} onDisplayChange={setLocalDisplay} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function WorkspaceViewsDisplayDropdown({
label="Display"
icon={<IconSliders />}
displayValue="Display"
panelClassName="flex min-w-[280px] max-w-[320px] flex-col rounded-md border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised) overflow-hidden"
panelClassName="flex w-[min(320px,calc(100vw-2rem))] max-h-[min(52vh,22rem)] flex-col overflow-hidden rounded-md border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)"
align="right"
>
<WorkspaceViewsDisplayPanel display={display} onDisplayChange={setDisplay} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function WorkspaceViewsDisplayPanel({
<div className="border-b border-(--border-subtle) bg-(--bg-surface-1) p-3">
<p className="text-xs font-medium text-(--txt-secondary)">Display Properties</p>
</div>
<div className="flex flex-1 flex-wrap gap-2 p-3">
<div className="flex flex-1 flex-wrap content-start gap-2 overflow-y-auto p-3">
{DISPLAY_PROPERTY_KEYS.map((key) => {
const selected = display.properties.includes(key);
return (
Expand Down
18 changes: 16 additions & 2 deletions ui/src/components/workspace-views/WorkspaceViewsEllipsisMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ const IconMoreVertical = () => (
</svg>
);

const DROPDOWN_Z_INDEX = 9999;
const DROPDOWN_Z_INDEX = 10100;
const VIEWPORT_PADDING = 8;
const PANEL_GAP = 4;

export function WorkspaceViewsEllipsisMenu() {
const location = useLocation();
Expand All @@ -57,6 +59,7 @@ export function WorkspaceViewsEllipsisMenu() {
const [position, setPosition] = useState<{
top: number;
right: number;
maxHeight?: number;
} | null>(null);

const fullUrl =
Expand All @@ -68,10 +71,20 @@ export function WorkspaceViewsEllipsisMenu() {
return;
}
const rect = triggerRef.current.getBoundingClientRect();
const panelRect = panelRef.current?.getBoundingClientRect();
const panelHeight = panelRect?.height ?? 0;
const availableBelow = window.innerHeight - rect.bottom - VIEWPORT_PADDING - PANEL_GAP;
const availableAbove = rect.top - VIEWPORT_PADDING - PANEL_GAP;
let top = rect.bottom + PANEL_GAP;
if (panelHeight > 0 && availableBelow < panelHeight && availableAbove > availableBelow) {
top = Math.max(VIEWPORT_PADDING, rect.top - panelHeight - PANEL_GAP);
}
const maxHeight = Math.max(120, Math.max(availableBelow, availableAbove));
Comment on lines 73 to +82
queueMicrotask(() =>
setPosition({
top: rect.bottom + 4,
top,
right: window.innerWidth - rect.right,
maxHeight,
}),
);
}, [open]);
Expand Down Expand Up @@ -123,6 +136,7 @@ export function WorkspaceViewsEllipsisMenu() {
position: 'fixed',
top: position.top,
right: position.right,
...(position.maxHeight !== undefined && { maxHeight: position.maxHeight }),
zIndex: DROPDOWN_Z_INDEX,
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function WorkspaceViewsFiltersDropdown({
label="Filters"
icon={<FILTER_ICONS.filter />}
displayValue="Filters"
panelClassName="flex w-[280px] max-h-[min(70vh,28rem)] flex-col rounded-md border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised) overflow-hidden"
panelClassName="flex w-[min(280px,calc(100vw-2rem))] max-h-[min(52vh,22rem)] flex-col overflow-hidden rounded-md border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)"
align="right"
>
<WorkspaceViewsFiltersPanel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ import type {
LabelApiResponse,
} from '../../api/types';

const LONG_LIST_PANEL_STYLE = { maxHeight: 'min(70vh, 28rem)' };

export interface WorkspaceViewsFiltersPanelProps {
filters: WorkspaceViewFilters;
onFiltersChange: (updater: (prev: WorkspaceViewFilters) => WorkspaceViewFilters) => void;
Expand Down Expand Up @@ -130,10 +128,7 @@ export function WorkspaceViewsFiltersPanel({
!search.trim() || label.toLowerCase().includes(search.trim().toLowerCase());

const content = (
<div
className={compact ? 'min-h-0 flex-1 overflow-y-auto py-1' : 'space-y-0'}
style={compact ? LONG_LIST_PANEL_STYLE : undefined}
>
<div className={compact ? 'min-h-0 flex-1 overflow-y-auto py-1' : 'space-y-0'}>
<CollapsibleSection
title="Priority"
open={sectionOpen.priority}
Expand Down
Loading