Skip to content

Commit cac78e2

Browse files
committed
feat: preview roadmap items
1 parent 20f64fd commit cac78e2

3 files changed

Lines changed: 161 additions & 32 deletions

File tree

frontend/src/lib/features/collections/BoardItemCard.svelte

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
swimlaneParentId = '',
2828
cardStyle = 'background-color: var(--ds-surface-raised); border-color: var(--ds-border);',
2929
textStyle = 'color: var(--ds-text);',
30+
showMoveMenu = true,
3031
onopen = null,
3132
} = $props();
3233
@@ -92,19 +93,21 @@
9293
<h4 class="min-w-0 flex-1 break-words text-sm font-medium leading-5" style={textStyle}>
9394
{item.title}
9495
</h4>
95-
<div class="board-card-menu -mr-1 -mt-1 shrink-0 transition-opacity">
96-
<DropdownMenu
97-
items={resolvedMoveMenuItems}
98-
placement="bottom-end"
99-
maxWidth="max-w-xs"
100-
triggerIcon={MoreHorizontal}
101-
triggerClass="p-1 rounded hover:bg-[var(--ds-background-neutral-hovered)] focus:ring-2 focus:ring-[var(--ds-border-focused)]"
102-
triggerStyle="color: var(--ds-text-subtle);"
103-
iconOnly
104-
showChevron={false}
105-
triggerTestid={`board-card-move-menu-${item.id}`}
106-
/>
107-
</div>
96+
{#if showMoveMenu}
97+
<div class="board-card-menu -mr-1 -mt-1 shrink-0 transition-opacity">
98+
<DropdownMenu
99+
items={resolvedMoveMenuItems}
100+
placement="bottom-end"
101+
maxWidth="max-w-xs"
102+
triggerIcon={MoreHorizontal}
103+
triggerClass="p-1 rounded hover:bg-[var(--ds-background-neutral-hovered)] focus:ring-2 focus:ring-[var(--ds-border-focused)]"
104+
triggerStyle="color: var(--ds-text-subtle);"
105+
iconOnly
106+
showChevron={false}
107+
triggerTestid={`board-card-move-menu-${item.id}`}
108+
/>
109+
</div>
110+
{/if}
108111
</div>
109112
110113
{#if bodyCardFields.length > 0}

frontend/src/lib/features/collections/CollectionRoadmap.svelte

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
import SubFilterBar from './SubFilterBar.svelte';
1616
import Select from '../../components/Select.svelte';
1717
import ItemDetail from '../items/ItemDetail.svelte';
18-
import { Settings, ChevronLeft, ChevronRight, Diamond, ChevronDown, Circle, GitBranch } from '@lucide/svelte';
18+
import RoadmapItemPreview from './RoadmapItemPreview.svelte';
19+
import { Settings, ChevronLeft, ChevronRight, Diamond, ChevronDown, Circle, GitBranch, CalendarClock } from '@lucide/svelte';
1920
import { getVisibleColor } from '../../utils/colorUtils.js';
2021
import { itemTypeIconMap } from '../../utils/icons.js';
2122
import { SYSTEM_FIELDS } from '../../stores/fieldConfig.js';
@@ -65,6 +66,7 @@
6566
let boardConfig = $state(null);
6667
let boardConfigId = $state(null);
6768
let roadmapConfig = $state({ start_field_id: 'due_date', end_field_id: '', dependency_link_type_id: null });
69+
let roadmapCardFields = $derived(boardConfig?.card_fields || []);
6870
let linkTypes = $state([]);
6971
let customFields = $state([]);
7072
let screenFields = $state([]);
@@ -412,6 +414,10 @@
412414
return !!(start || end);
413415
}
414416
417+
let unscheduledItemCount = $derived(
418+
collectionStore.loading ? 0 : collectionStore.items.filter((item) => !itemHasDate(item)).length,
419+
);
420+
415421
// Auto-expand root items with children on first load
416422
$effect(() => {
417423
if (!collectionStore.loading && collectionStore.items.length > 0) {
@@ -1045,6 +1051,17 @@
10451051
10461052
</div>
10471053
1054+
{#if roadmapConfig.start_field_id && unscheduledItemCount > 0}
1055+
<div
1056+
class="mb-4 flex items-center gap-2 rounded-md px-3 py-2 text-xs"
1057+
style="background-color: var(--ds-background-neutral-hovered); color: var(--ds-text-subtle);"
1058+
data-testid="roadmap-unscheduled-hint"
1059+
>
1060+
<CalendarClock class="h-4 w-4 shrink-0" />
1061+
<span>{unscheduledItemCount} {unscheduledItemCount === 1 ? 'item needs dates' : 'items need dates'} — click a timeline row to schedule it.</span>
1062+
</div>
1063+
{/if}
1064+
10481065
10491066
<!-- No config state -->
10501067
{#if !roadmapConfig.start_field_id}
@@ -1246,28 +1263,60 @@
12461263
12471264
{#if roadmapItem.isMilestone && !roadmapConfig.end_field_id}
12481265
<!-- Diamond milestone (no end field configured, can't expand) -->
1249-
<div
1250-
data-testid="roadmap-bar-{item.id}"
1251-
data-start-date={roadmapItem.startDate || ''}
1252-
data-end-date={roadmapItem.endDate || ''}
1253-
class="absolute flex items-center justify-center"
1254-
style="left: {barLeftPx + barWidthPx / 2 - 8}px; top: {(ROW_HEIGHT - 16) / 2}px; width: 16px; height: 16px; transform: rotate(45deg); background-color: {visibleColor}; border-radius: 2px; z-index: 10; cursor: grab;"
1255-
role="button"
1256-
tabindex="0"
1266+
<RoadmapItemPreview
1267+
item={roadmapItem}
1268+
workspace={workspace}
1269+
itemTypes={itemTypes}
1270+
cardFields={roadmapCardFields}
1271+
priorities={workspaceDataStore.priorities}
1272+
statuses={statuses}
1273+
iterations={workspaceDataStore.iterations}
1274+
projects={workspaceDataStore.projects}
1275+
labels={workspaceDataStore.labels}
1276+
customFieldDefinitions={customFields}
1277+
users={workspaceDataStore.users}
12571278
onpointerdown={(e) => onBarPointerDown(e, roadmapItem, 'move')}
1258-
></div>
1279+
onopen={openItem}
1280+
>
1281+
{#snippet children()}
1282+
<div
1283+
data-testid="roadmap-bar-{item.id}"
1284+
data-start-date={roadmapItem.startDate || ''}
1285+
data-end-date={roadmapItem.endDate || ''}
1286+
class="absolute flex items-center justify-center"
1287+
style="left: {barLeftPx + barWidthPx / 2 - 8}px; top: {(ROW_HEIGHT - 16) / 2}px; width: 16px; height: 16px; transform: rotate(45deg); background-color: {visibleColor}; border-radius: 2px; z-index: 10; cursor: grab;"
1288+
role="button"
1289+
tabindex="0"
1290+
></div>
1291+
{/snippet}
1292+
</RoadmapItemPreview>
12591293
{:else}
12601294
<!-- Range bar OR expandable milestone (thin bar with resize handles) -->
1261-
<div
1262-
data-testid="roadmap-bar-{item.id}"
1263-
data-start-date={roadmapItem.startDate || ''}
1264-
data-end-date={roadmapItem.endDate || ''}
1265-
class="absolute flex items-center rounded group/bar"
1266-
style="left: {barLeftPx}px; width: {barWidthPx}px; top: {(ROW_HEIGHT - 24) / 2}px; height: 24px; background-color: {visibleColor}; opacity: 0.85; z-index: 10; cursor: grab;"
1267-
role="button"
1268-
tabindex="0"
1295+
<RoadmapItemPreview
1296+
item={roadmapItem}
1297+
workspace={workspace}
1298+
itemTypes={itemTypes}
1299+
cardFields={roadmapCardFields}
1300+
priorities={workspaceDataStore.priorities}
1301+
statuses={statuses}
1302+
iterations={workspaceDataStore.iterations}
1303+
projects={workspaceDataStore.projects}
1304+
labels={workspaceDataStore.labels}
1305+
customFieldDefinitions={customFields}
1306+
users={workspaceDataStore.users}
12691307
onpointerdown={(e) => onBarPointerDown(e, roadmapItem, 'move')}
1308+
onopen={openItem}
12701309
>
1310+
{#snippet children()}
1311+
<div
1312+
data-testid="roadmap-bar-{item.id}"
1313+
data-start-date={roadmapItem.startDate || ''}
1314+
data-end-date={roadmapItem.endDate || ''}
1315+
class="absolute flex items-center rounded group/bar"
1316+
style="left: {barLeftPx}px; width: {barWidthPx}px; top: {(ROW_HEIGHT - 24) / 2}px; height: 24px; background-color: {visibleColor}; opacity: 0.85; z-index: 10; cursor: grab;"
1317+
role="button"
1318+
tabindex="0"
1319+
>
12711320
<div
12721321
class="absolute left-0 top-0 bottom-0 w-2 cursor-col-resize opacity-0 group-hover/bar:opacity-100 rounded-l"
12731322
style="background-color: rgba(0,0,0,0.2);"
@@ -1289,7 +1338,9 @@
12891338
tabindex="-1"
12901339
onpointerdown={(e) => { e.stopPropagation(); onBarPointerDown(e, roadmapItem, 'resize-right'); }}
12911340
></div>
1292-
</div>
1341+
</div>
1342+
{/snippet}
1343+
</RoadmapItemPreview>
12931344
{/if}
12941345
{:else if schedulePreview?.itemId === item.id}
12951346
{@const previewColor = getVisibleColor(statuses.find(s => s.id === item.status_id)?.color || '#6b7280')}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<script>
2+
import { createPopover, melt } from '@melt-ui/svelte';
3+
import BoardItemCard from './BoardItemCard.svelte';
4+
5+
let {
6+
item,
7+
workspace,
8+
itemTypes = [],
9+
cardFields = [],
10+
priorities = [],
11+
statuses = [],
12+
iterations = [],
13+
projects = [],
14+
labels = [],
15+
customFieldDefinitions = [],
16+
users = [],
17+
onpointerdown = null,
18+
onopen = null,
19+
children,
20+
} = $props();
21+
22+
const {
23+
elements: { trigger, content },
24+
states: { open },
25+
} = createPopover({
26+
forceVisible: true,
27+
positioning: {
28+
strategy: 'fixed',
29+
placement: 'bottom-start',
30+
gutter: 8,
31+
flip: true,
32+
},
33+
portal: 'body',
34+
});
35+
36+
function openItem(event) {
37+
event.stopPropagation();
38+
$open = false;
39+
onopen?.(item.id);
40+
}
41+
</script>
42+
43+
<div use:melt={$trigger} onpointerdown={onpointerdown}>
44+
{@render children()}
45+
</div>
46+
47+
{#if $open}
48+
<div
49+
use:melt={$content}
50+
class="roadmap-item-preview z-[60] w-[min(20rem,calc(100vw-2rem))]"
51+
data-testid={`roadmap-item-preview-${item.id}`}
52+
>
53+
<BoardItemCard
54+
{item}
55+
{workspace}
56+
{itemTypes}
57+
{cardFields}
58+
{priorities}
59+
{statuses}
60+
{iterations}
61+
{projects}
62+
{labels}
63+
{customFieldDefinitions}
64+
{users}
65+
showMoveMenu={false}
66+
onopen={openItem}
67+
/>
68+
</div>
69+
{/if}
70+
71+
<style>
72+
.roadmap-item-preview :global(.board-card) {
73+
box-shadow: var(--ds-shadow-raised);
74+
}
75+
</style>

0 commit comments

Comments
 (0)