Skip to content

Commit 05a27a5

Browse files
authored
Merge pull request #44 from mayank1513/feat/39-custom-color
Feat/39 custom color
2 parents 332f1f4 + 5b576c3 commit 05a27a5

14 files changed

+244
-85
lines changed

.tkb

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"scope": "Workspace",
3+
"tasks": {
4+
"task-MMVBTdvRS-hXujgWL4gLW": {
5+
"id": "task-MMVBTdvRS-hXujgWL4gLW",
6+
"description": "Create new tasks\n\nTask can also contain lists\n\n- list item 1\n\n- list item 2",
7+
"columnId": "column-todo"
8+
}
9+
},
10+
"columns": [
11+
{
12+
"id": "column-todo",
13+
"title": "To do",
14+
"tasksIds": [
15+
"task-MMVBTdvRS-hXujgWL4gLW"
16+
]
17+
},
18+
{
19+
"id": "column-doing",
20+
"title": "Doing",
21+
"tasksIds": []
22+
},
23+
{
24+
"id": "column-done",
25+
"title": "Done",
26+
"tasksIds": []
27+
}
28+
]
29+
}

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Trello Kanban Board
22

3+
## 0.7.0
4+
5+
### Minor Changes
6+
7+
- 9ea8bcd: You can now select custom colors for your taks and columns.
8+
39
## 0.6.1
410

511
### Patch Changes

extension/interface.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ export interface TaskType {
99
id: string;
1010
description: string;
1111
columnId: string;
12+
color?: string;
1213
}
1314

1415
export interface ColumnType {
1516
id: string;
1617
title: string;
18+
color?: string;
1719
description?: string;
1820
archived?: boolean;
1921
tasksIds: string[];

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "trello-kanban-task-board",
33
"private": true,
4-
"version": "0.6.1",
4+
"version": "0.7.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite --port 3000",
@@ -19,6 +19,7 @@
1919
"nextjs-themes": "^2.1.1",
2020
"react": "^18.2.0",
2121
"react-beautiful-dnd": "^13.1.1",
22+
"react-color-palette": "^7.2.0",
2223
"react-dom": "^18.2.0",
2324
"react-markdown": "^9.0.1",
2425
"react-toastify": "^9.1.3",

src/App.test.tsx

+18-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ describe("Test Board", () => {
137137
test("Edit task", async ({ expect }) => {
138138
const columnEl = screen.getByTestId("column-0");
139139
const taskEl = columnEl.getElementsByClassName(taskStyles.task)[0];
140-
const editBtn = taskEl.getElementsByTagName("button")[0];
140+
const editBtn = taskEl.getElementsByTagName("button")[1];
141141
act(() => fireEvent.click(editBtn));
142142
const inputEl = taskEl.getElementsByTagName("textarea")[0];
143143
act(() => fireEvent.input(inputEl, { target: { value: "Task 1" } }));
@@ -170,4 +170,21 @@ describe("Test Board", () => {
170170
expect(el).toBeDefined();
171171
act(() => fireEvent.click(el));
172172
});
173+
174+
test("Set Column color renders", () => {
175+
const columnEl = screen.getByTestId("column-0");
176+
const colorBtn = columnEl
177+
.getElementsByClassName(columnListStyles.headerContent)[0]
178+
.getElementsByTagName("button")[0];
179+
act(() => fireEvent.click(colorBtn));
180+
act(() => fireEvent.click(screen.getByText("Ok")));
181+
});
182+
183+
test("Set Task color renders", () => {
184+
const columnEl = screen.getByTestId("column-0");
185+
const taskHEaderEl = columnEl.getElementsByClassName(taskStyles.task)[0].getElementsByTagName("header")[0];
186+
const colorBtn = taskHEaderEl.getElementsByTagName("button")[0];
187+
act(() => fireEvent.click(colorBtn));
188+
act(() => fireEvent.click(screen.getByText("Ok")));
189+
});
173190
});

src/components/color-selector.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ColorPicker, useColor } from "react-color-palette";
2+
import "react-color-palette/css";
3+
4+
export function ColorSelector(props: { color?: string; setColor: (color: string) => void; onClose: () => void }) {
5+
const [color, setColor] = useColor(props.color || "gray");
6+
return (
7+
<div className="modal">
8+
<div className="content">
9+
<ColorPicker hideInput={["rgb", "hsv"]} color={color} onChange={setColor} />
10+
<button onClick={() => props.setColor(color.hex)}>Ok</button>
11+
<button onClick={props.onClose}>Cancel</button>
12+
</div>
13+
</div>
14+
);
15+
}

src/components/column-list/column-header.tsx

+33-19
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { HTMLProps, useId, useState } from "react";
33
import styles from "./column-list.module.scss";
44
import { useGlobalState } from "utils/context";
55
import { vscode } from "utils/vscode";
6+
import { ColorSelector } from "components/color-selector";
67

78
export default function ColumnHeader(props: HTMLProps<HTMLElement> & { column: ColumnType }) {
89
const { state, setState } = useGlobalState();
910
const { column, ...rest } = props;
1011
const [text, setText] = useState(column.title);
12+
const [showColorSelector, setShowColorSelector] = useState(false);
1113
const [isEditing, setIsEditing] = useState(false);
1214
const id = useId();
1315
const onBlur = () => {
@@ -25,25 +27,37 @@ export default function ColumnHeader(props: HTMLProps<HTMLElement> & { column: C
2527
setState({ ...state, columns: state.columns.filter((c) => c !== column), tasks });
2628
vscode.toast(`"${column.title}" column and ${column.tasksIds.length} tasks deleted!`, "warn");
2729
};
30+
const setColor = (color: string) => {
31+
column.color = color;
32+
setState({ ...state, columns: [...state.columns] });
33+
setShowColorSelector(false);
34+
};
2835
return (
29-
<header {...rest} className={styles.header}>
30-
<label htmlFor={id} onClick={() => setIsEditing(true)}>
31-
<input
32-
type="text"
33-
id={id}
34-
value={text}
35-
onChange={(e) => setText(e.target.value)}
36-
onBlur={onBlur}
37-
placeholder="Add column title..."
38-
hidden={!isEditing}
39-
/>
40-
<div hidden={isEditing} className={styles.headerContent}>
41-
{column.title || <span className={styles.placeholder}>Add column title</span>}{" "}
42-
<span className={styles.close} onClick={removeColumn}>
43-
44-
</span>
45-
</div>
46-
</label>
47-
</header>
36+
<>
37+
<header {...rest} className={styles.header}>
38+
<label htmlFor={id} onClick={() => setIsEditing(true)}>
39+
<input
40+
type="text"
41+
id={id}
42+
value={text}
43+
onChange={(e) => setText(e.target.value)}
44+
onBlur={onBlur}
45+
placeholder="Add column title..."
46+
hidden={!isEditing}
47+
/>
48+
<div hidden={isEditing} className={styles.headerContent}>
49+
{column.title || <span className={styles.placeholder}>Add column title</span>}
50+
<div className="grow"></div>
51+
<button onClick={() => setShowColorSelector(true)}>🖌</button>
52+
<button className={styles.close} onClick={removeColumn}>
53+
54+
</button>
55+
</div>
56+
</label>
57+
</header>
58+
{showColorSelector && (
59+
<ColorSelector color={column.color} setColor={setColor} onClose={() => setShowColorSelector(false)} />
60+
)}
61+
</>
4862
);
4963
}

src/components/column-list/column-list.module.scss

+15-9
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,28 @@
2323
background: var(--bg);
2424
}
2525
.header {
26-
&:hover .close {
26+
button {
27+
all: unset;
28+
display: none;
29+
padding: 0 5px;
30+
cursor: pointer;
31+
}
32+
&:hover button {
2733
display: inline-block;
2834
}
35+
button:hover {
36+
color: orange;
37+
}
38+
39+
.close:hover {
40+
color: red;
41+
}
2942
}
3043
.headerContent {
3144
cursor: grab;
3245
display: flex;
3346
padding: 0 5px;
34-
justify-content: space-between;
47+
align-items: center;
3548
}
3649
.placeholder {
3750
opacity: 0.5;
@@ -56,10 +69,3 @@
5669
padding: 0 10px;
5770
margin: 0 -10px;
5871
}
59-
.close {
60-
display: none;
61-
cursor: pointer;
62-
&:hover {
63-
color: red;
64-
}
65-
}

src/components/column-list/column.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function Column({ column, index }: { column: ColumnType; index: n
2424
// listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
2525
const newTaskElement = listRef.current?.children[listRef.current.children.length - 1] as HTMLLabelElement;
2626
newTaskElement?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
27-
newTaskElement?.getElementsByTagName("button")[0].click();
27+
newTaskElement?.getElementsByTagName("button")[1].click();
2828
setTimeout(() => {
2929
newTaskElement?.getElementsByTagName("textarea")[0].focus();
3030
newTaskElement?.getElementsByTagName("textarea")[0].click();
@@ -47,7 +47,7 @@ export default function Column({ column, index }: { column: ColumnType; index: n
4747
<Droppable droppableId={column.id} direction="vertical">
4848
{(provided1) => (
4949
<div ref={provided1.innerRef} {...provided1.droppableProps} className={styles.columnDropzone}>
50-
<div className={styles.column}>
50+
<div className={styles.column} style={{ background: column.color }}>
5151
<ColumnHeader column={column} {...provided.dragHandleProps} />
5252
<hr />
5353
<ul className={styles.taskList} ref={listRef}>

src/components/task/index.tsx

+71-52
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import remarkGfm from "remark-gfm";
77
import { useGlobalState } from "utils/context";
88
import styles from "./task.module.scss";
99
import { vscode } from "utils/vscode";
10+
import { ColorSelector } from "components/color-selector";
1011

1112
function resizeTextArea(textareaRef: RefObject<HTMLTextAreaElement>) {
1213
const target = textareaRef.current;
@@ -18,6 +19,7 @@ function resizeTextArea(textareaRef: RefObject<HTMLTextAreaElement>) {
1819
export default function Task({ task, index }: { task: TaskType; index: number }) {
1920
const { state, setState } = useGlobalState();
2021
const [isEditing, setIsEditing] = useState(false);
22+
const [showColorSelector, setShowColorSelector] = useState(false);
2123
const textareaRef = useRef<HTMLTextAreaElement>(null);
2224
const id = useId();
2325

@@ -32,58 +34,75 @@ export default function Task({ task, index }: { task: TaskType; index: number })
3234
setState({ ...state, tasks, columns });
3335
vscode.toast("Task deleted!", "success");
3436
};
37+
38+
const setColor = (color: string) => {
39+
task.color = color;
40+
setState({ ...state, tasks: { ...state.tasks } });
41+
setShowColorSelector(false);
42+
};
3543
return (
36-
<Draggable draggableId={task.id} index={index}>
37-
{(provided, snapshot) => {
38-
if (snapshot.isDragging && provided.draggableProps.style?.transform)
39-
provided.draggableProps.style.transform += " rotate(5deg)";
40-
return (
41-
<label
42-
htmlFor={id}
43-
ref={provided.innerRef}
44-
{...provided.draggableProps}
45-
className={[styles.task, isEditing ? styles.active : ""].join(" ")}>
46-
<header {...provided.dragHandleProps}>
47-
<span>∘∘∘</span>
48-
<button
49-
onClick={() => {
50-
setIsEditing(true);
51-
if (textareaRef.current?.value) {
52-
textareaRef.current.value = "hk";
53-
textareaRef.current.value = task.description;
54-
}
55-
setTimeout(() => resizeTextArea(textareaRef), 100);
56-
}}>
57-
🖉
58-
</button>
59-
<button className={styles.close} onClick={removeTask}>
60-
61-
</button>
62-
</header>
63-
<textarea
64-
id={id}
65-
value={task.description}
66-
ref={textareaRef}
67-
onChange={(e) => {
68-
task.description = e.target.value.replace(/ +/, " ").replace(/\n\n+/g, "\n\n");
69-
setState({ ...state, tasks: { ...state.tasks } });
70-
resizeTextArea(textareaRef);
71-
}}
72-
onBlur={() => setIsEditing(false)}
73-
placeholder="Enter task description in Markdown format"
74-
hidden={!isEditing}
75-
/>
76-
{!isEditing &&
77-
(task.description.trim() ? (
78-
<ReactMarkdown rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
79-
{task.description.replace(/\n+/g, "\n\n")}
80-
</ReactMarkdown>
81-
) : (
82-
<p className={styles.placeholder}>Enter task description in Markdown format.</p>
83-
))}
84-
</label>
85-
);
86-
}}
87-
</Draggable>
44+
<>
45+
<Draggable draggableId={task.id} index={index}>
46+
{(provided, snapshot) => {
47+
if (snapshot.isDragging && provided.draggableProps.style?.transform)
48+
provided.draggableProps.style.transform += " rotate(5deg)";
49+
50+
return (
51+
<div ref={provided.innerRef} {...provided.draggableProps}>
52+
<label
53+
htmlFor={id}
54+
className={[styles.task, isEditing ? styles.active : ""].join(" ")}
55+
style={{ background: task.color }}>
56+
<header {...provided.dragHandleProps}>
57+
<span>∘∘∘</span>
58+
<button onClick={() => setShowColorSelector(true)}>🖌</button>
59+
<button
60+
onClick={() => {
61+
setIsEditing(true);
62+
if (textareaRef.current?.value) {
63+
textareaRef.current.value = "hk";
64+
textareaRef.current.value = task.description;
65+
}
66+
setTimeout(() => {
67+
textareaRef.current && textareaRef.current.focus();
68+
resizeTextArea(textareaRef);
69+
}, 100);
70+
}}>
71+
🖉
72+
</button>
73+
<button className={styles.close} onClick={removeTask}>
74+
75+
</button>
76+
</header>
77+
<textarea
78+
id={id}
79+
value={task.description}
80+
ref={textareaRef}
81+
onChange={(e) => {
82+
task.description = e.target.value.replace(/ +/, " ").replace(/\n\n+/g, "\n\n");
83+
setState({ ...state, tasks: { ...state.tasks } });
84+
resizeTextArea(textareaRef);
85+
}}
86+
onBlur={() => setIsEditing(false)}
87+
placeholder="Enter task description in Markdown format"
88+
hidden={!isEditing}
89+
/>
90+
{!isEditing &&
91+
(task.description.trim() ? (
92+
<ReactMarkdown rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
93+
{task.description.replace(/\n+/g, "\n\n")}
94+
</ReactMarkdown>
95+
) : (
96+
<p className={styles.placeholder}>Enter task description in Markdown format.</p>
97+
))}
98+
</label>
99+
</div>
100+
);
101+
}}
102+
</Draggable>
103+
{showColorSelector && (
104+
<ColorSelector color={task.color} setColor={setColor} onClose={() => setShowColorSelector(false)} />
105+
)}
106+
</>
88107
);
89108
}

0 commit comments

Comments
 (0)