Skip to content

Commit a18f0e0

Browse files
committed
[Projects]: Implement infinitely nested projects
1 parent efbf630 commit a18f0e0

File tree

17 files changed

+594
-34
lines changed

17 files changed

+594
-34
lines changed

src/lib/components/board/board.svelte

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,25 @@
99
import type { Card } from '$lib/components/task-card/types';
1010
import Container from '@awenovations/aura/container.svelte';
1111
import TaskForm from '$lib/components/task-form/task-form.svelte';
12+
import Breadcrumb from '$lib/components/breadcrumb/breadcrumb.svelte';
13+
14+
interface BreadcrumbItem {
15+
_id: string;
16+
title: string;
17+
}
1218
1319
interface Props {
1420
handleSubmit: (event: FormEvent<HTMLFormElement>) => void;
1521
cards?: Array<Card>;
1622
openTaskId?: string;
1723
editTaskId?: string;
24+
projectId?: string;
25+
breadcrumb?: Array<BreadcrumbItem>;
1826
}
1927
20-
let { handleSubmit, cards = [], openTaskId, editTaskId }: Props = $props();
28+
let { handleSubmit, cards = [], openTaskId, editTaskId, projectId, breadcrumb }: Props = $props();
29+
30+
const basePath = $derived(projectId ? `/app/project/${projectId}` : '/app');
2131
2232
let openTask: Partial<Card> = $state({});
2333
let openEditedTask: Partial<Card> = $state({});
@@ -32,7 +42,7 @@
3242
3343
const clearTaskRoute = () => {
3444
if (browser) {
35-
history.replaceState({}, '', '/app');
45+
history.replaceState({}, '', basePath);
3646
}
3747
};
3848
@@ -44,18 +54,24 @@
4454
taskDetailsOpen = true;
4555
openTask = _task;
4656
if (browser && _task._id) {
47-
history.pushState({}, '', '/app/task/' + _task._id);
57+
history.pushState({}, '', basePath + '/task/' + _task._id);
4858
}
4959
}, 500);
5060
} else {
5161
taskDetailsOpen = true;
5262
openTask = _task;
5363
if (browser && _task._id) {
54-
history.pushState({}, '', '/app/task/' + _task._id);
64+
history.pushState({}, '', basePath + '/task/' + _task._id);
5565
}
5666
}
5767
};
5868
69+
const handleOpenProject = (_card) => {
70+
if (browser) {
71+
window.location.href = '/app/project/' + _card._id;
72+
}
73+
};
74+
5975
let hasAutoOpened = false;
6076
let hasAutoEdited = false;
6177
@@ -71,7 +87,8 @@
7187
assignee: card.assignee,
7288
createDate: card.createDate,
7389
type: card.taskType,
74-
column: card.column
90+
column: card.column,
91+
cardType: card.cardType
7592
};
7693
untrack(() => handleOpenTask(task));
7794
}
@@ -90,7 +107,8 @@
90107
assignee: card.assignee,
91108
createDate: card.createDate,
92109
type: card.taskType,
93-
column: card.column
110+
column: card.column,
111+
cardType: card.cardType
94112
};
95113
untrack(() => handleEditTask(task));
96114
}
@@ -105,14 +123,14 @@
105123
taskFormOpen = true;
106124
openEditedTask = _task;
107125
if (browser && _task._id) {
108-
history.pushState({}, '', '/app/task/' + _task._id + '/edit');
126+
history.pushState({}, '', basePath + '/task/' + _task._id + '/edit');
109127
}
110128
}, 500);
111129
} else {
112130
taskFormOpen = true;
113131
openEditedTask = _task;
114132
if (browser && _task._id) {
115-
history.pushState({}, '', '/app/task/' + _task._id + '/edit');
133+
history.pushState({}, '', basePath + '/task/' + _task._id + '/edit');
116134
}
117135
}
118136
};
@@ -162,6 +180,7 @@
162180
<svelte:window onkeydown={handleEscapeKeydown} />
163181

164182
<div class="filter-wrapper">
183+
<Breadcrumb {breadcrumb} />
165184
<TextField
166185
class="card-filter"
167186
type="search"
@@ -174,27 +193,31 @@
174193
<Column
175194
{handleOpenTask}
176195
{handleEditTask}
196+
{handleOpenProject}
177197
cards={tasksByColumns['Backlog']}
178198
name="Backlog"
179199
{handleCreateTask}
180200
/>
181201
<Column
182202
{handleOpenTask}
183203
{handleEditTask}
204+
{handleOpenProject}
184205
cards={tasksByColumns['To Do']}
185206
name="To Do"
186207
{handleCreateTask}
187208
/>
188209
<Column
189210
{handleOpenTask}
190211
{handleEditTask}
212+
{handleOpenProject}
191213
cards={tasksByColumns['In Progress']}
192214
name="In Progress"
193215
{handleCreateTask}
194216
/>
195217
<Column
196218
{handleOpenTask}
197219
{handleEditTask}
220+
{handleOpenProject}
198221
cards={tasksByColumns['Done']}
199222
name="Done"
200223
{handleCreateTask}
@@ -204,7 +227,7 @@
204227
{#if taskFormOpen}
205228
<div class="task-panel">
206229
<h4>New Task</h4>
207-
<TaskForm task={openEditedTask} {handleClose} column={newTaskColumn} {handleSubmit} />
230+
<TaskForm task={openEditedTask} {handleClose} column={newTaskColumn} {handleSubmit} {projectId} />
208231
</div>
209232
{/if}
210233

@@ -262,9 +285,9 @@
262285
width: 20rem;
263286
}
264287
265-
266288
display: flex;
267-
justify-content: flex-end;
289+
justify-content: space-between;
290+
align-items: flex-end;
268291
margin-bottom: 1rem;
269292
}
270293
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<script lang="ts">
2+
interface BreadcrumbItem {
3+
_id: string;
4+
title: string;
5+
}
6+
7+
interface Props {
8+
breadcrumb?: Array<BreadcrumbItem>;
9+
}
10+
11+
let { breadcrumb = [] }: Props = $props();
12+
13+
const maxVisible = 5;
14+
const shouldCollapse = $derived(breadcrumb.length > maxVisible);
15+
// When collapsing: show first crumb, ellipsis linking to parent of the last 3, then last 3 crumbs
16+
const tailCount = 3;
17+
const ellipsisTarget = $derived(shouldCollapse ? breadcrumb[breadcrumb.length - tailCount - 1] : null);
18+
const visibleCrumbs = $derived(
19+
shouldCollapse
20+
? [breadcrumb[0], ...breadcrumb.slice(-tailCount)]
21+
: breadcrumb
22+
);
23+
</script>
24+
25+
<nav class="breadcrumb">
26+
Projects: <a href="/app" class="breadcrumb-link">Main</a>
27+
{#each visibleCrumbs as crumb, index}
28+
<span class="breadcrumb-separator">&gt;</span>
29+
{#if shouldCollapse && index === 1}
30+
<a href="/app/project/{ellipsisTarget._id}" class="breadcrumb-link breadcrumb-ellipsis">&hellip;</a>
31+
<span class="breadcrumb-separator">&gt;</span>
32+
{/if}
33+
{#if index < visibleCrumbs.length - 1}
34+
<a href="/app/project/{crumb._id}" class="breadcrumb-link">{crumb.title}</a>
35+
{:else}
36+
<span class="breadcrumb-current">{crumb.title}</span>
37+
{/if}
38+
{/each}
39+
</nav>
40+
41+
<style lang="scss">
42+
.breadcrumb {
43+
display: flex;
44+
align-items: center;
45+
gap: 0.5rem;
46+
font: var(--aura-default-regular);
47+
48+
&, .breadcrumb-link {
49+
color: var(--aura-light-tertiary-30);
50+
}
51+
}
52+
53+
.breadcrumb-link {
54+
text-decoration: none;
55+
56+
&:hover {
57+
text-decoration: underline;
58+
}
59+
}
60+
61+
.breadcrumb-current {
62+
font: var(--aura-default-semibold);
63+
}
64+
</style>

src/lib/components/column/column.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
handleCreateTask: (type: string) => void;
2020
handleEditTask: (task: Card) => void;
2121
handleOpenTask: (task: Card) => void;
22+
handleOpenProject: (card: Card) => void;
2223
}
2324
24-
let { name, cards = [], handleCreateTask, handleEditTask, handleOpenTask }: Props = $props();
25+
let { name, cards = [], handleCreateTask, handleEditTask, handleOpenTask, handleOpenProject }: Props = $props();
2526
2627
let columnWrapper: HTMLDivElement = $state();
2728
let droppable = $state(false);
@@ -296,8 +297,10 @@
296297
createDate={card.createDate}
297298
assignee={card.assignee}
298299
column={card.column}
300+
cardType={card.cardType}
299301
{handleEditTask}
300302
{handleOpenTask}
303+
{handleOpenProject}
301304
/>
302305
{/if}
303306
{/key}

src/lib/components/task-card/task-card.svelte

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
column: string;
2929
type?: string;
3030
createDate?: number;
31+
cardType?: 'task' | 'project';
3132
handleEditTask: (task: Card) => void;
3233
handleOpenTask: (task: Card) => void;
34+
handleOpenProject: (card: Card) => void;
3335
}
3436
3537
let {
@@ -40,10 +42,14 @@
4042
column,
4143
createDate,
4244
type = 'user story',
45+
cardType = 'task',
4346
handleEditTask,
44-
handleOpenTask
47+
handleOpenTask,
48+
handleOpenProject
4549
}: Props = $props();
4650
51+
const isProject = $derived(cardType === 'project');
52+
4753
let hideActionsTransition = $state(false);
4854
4955
let showActions = $state(false);
@@ -271,8 +277,10 @@
271277
<span class="card-body ql-content" data-cy="task-card-body">{@html body}</span>
272278
<span class="card-assignee" data-cy="task-card-assignee">Assigned to <i>{assignee}</i></span>
273279
<span class="card-type" data-cy="task-card-type"
274-
><span class="card-type-text">{type}</span>
275-
{#if type === 'user story'}
280+
><span class="card-type-text">{isProject ? 'project' : type}</span>
281+
{#if isProject}
282+
<Icon name="plan" />
283+
{:else if type === 'user story'}
276284
<Icon name="user-story" />
277285
{:else if type === 'bug fix'}
278286
<Icon name="bug" />
@@ -311,18 +319,23 @@
311319
<div
312320
class="action-button"
313321
data-cy="task-open-details-button"
314-
onclick={() =>
315-
handleOpenTask({
316-
_id: id,
317-
title,
318-
body,
319-
assignee,
320-
createDate,
321-
type,
322-
column
323-
})}
322+
onclick={() => {
323+
if (isProject) {
324+
handleOpenProject({ _id: id, title, body, assignee, type, column, user_id: '', order: 0, cardType });
325+
} else {
326+
handleOpenTask({
327+
_id: id,
328+
title,
329+
body,
330+
assignee,
331+
createDate,
332+
type,
333+
column
334+
});
335+
}
336+
}}
324337
>
325-
<Icon class="action-button-icon" name="eye-open" />
338+
<Icon class="action-button-icon" name={isProject ? 'eye-open' : 'eye-open'} />
326339
</div>
327340
<Divider class="actions-divider" />
328341
<div
@@ -335,7 +348,8 @@
335348
assignee,
336349
createDate,
337350
type,
338-
column
351+
column,
352+
cardType
339353
})}
340354
data-cy="task-card-edit-button"
341355
>

src/lib/components/task-card/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ export interface Card {
77
user_id: string;
88
order: number;
99
column: string;
10+
cardType?: 'task' | 'project';
11+
project_id?: string | null;
1012
}
1113

src/lib/components/task-form/task-form.svelte

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
handleClose: () => void;
1111
task?: Partial<Card>;
1212
handleSubmit: (event: FormEvent<HTMLFormElement>) => void;
13+
projectId?: string;
1314
}
1415
15-
let { column = '', handleClose: _handleClose, task = {}, handleSubmit }: Props = $props();
16+
let { column = '', handleClose: _handleClose, task = {}, handleSubmit, projectId }: Props = $props();
17+
18+
let cardTypeValue = $state(task.cardType || 'task');
1619
1720
const originalColumn = column || task.column || '';
1821
let selectedColumn = $state(originalColumn);
@@ -113,6 +116,25 @@
113116
</div>
114117

115118
<div class="right-column">
119+
{#if !task._id}
120+
<div class="right-column-row">
121+
<Dropdown
122+
data-cy="card-type"
123+
fullWidth
124+
name="card-type-select"
125+
on:change={(event) => {
126+
cardTypeValue = event.detail.value;
127+
}}
128+
currentValue={cardTypeValue}
129+
>
130+
{#snippet label()}
131+
<span>Card type</span>
132+
{/snippet}
133+
<aura-option value="task">Task</aura-option>
134+
<aura-option value="project">Project</aura-option>
135+
</Dropdown>
136+
</div>
137+
{/if}
116138
<div class="right-column-row">
117139
<Dropdown
118140
data-cy="status"
@@ -200,6 +222,10 @@
200222
</div>
201223
</div>
202224
<input type="hidden" name="order" value={orderValue} />
225+
<input type="hidden" name="card-type" value={cardTypeValue} />
226+
{#if projectId}
227+
<input type="hidden" name="project-id" value={projectId} />
228+
{/if}
203229
{#if task._id}
204230
<input type="hidden" name="id" value={task._id} />
205231
{/if}

0 commit comments

Comments
 (0)