Skip to content

Commit

Permalink
- Drag n drop for tasks (#2)
Browse files Browse the repository at this point in the history
- Save task isBodyVisible info on local storage (#22)
  • Loading branch information
gilesv committed Sep 15, 2019
1 parent b71bad9 commit 5579c34
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 48 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@types/node": "12.6.8",
"@types/node-sass": "^4.11.0",
"@types/react": "16.8.23",
"@types/react-beautiful-dnd": "^11.0.3",
"@types/react-dom": "16.8.4",
"@types/react-redux": "^7.1.1",
"@types/react-transition-group": "^4.2.2",
Expand Down Expand Up @@ -60,6 +61,7 @@
"postcss-safe-parser": "4.0.1",
"react": "^16.8.6",
"react-app-polyfill": "^1.0.1",
"react-beautiful-dnd": "^11.0.5",
"react-contenteditable": "^3.3.1",
"react-dev-utils": "^9.0.1",
"react-dom": "^16.8.6",
Expand Down
6 changes: 5 additions & 1 deletion src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class Dashboard extends React.Component<Props> {

public deleteStory(story: Story, index: number) {
const selectedStory = this.props.selectedStory;
const isFirstStory = index === 0;
const isSelected = index === selectedStory;
const isAfterSelected = index > selectedStory;

for (let task of story.tasks) {
Expand All @@ -54,7 +56,9 @@ class Dashboard extends React.Component<Props> {

this.props.dispatch(removeStory(story.id));

if (!isAfterSelected) {
if (isFirstStory && isSelected) {
this.selectStory(0);
} else if (!isAfterSelected) {
this.selectStory(selectedStory - 1);
}
}
Expand Down
13 changes: 6 additions & 7 deletions src/components/TaskItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface Props {
addTaskAtIndex: (index: number) => void,
updateTask: (task: Task) => void,
removeTask: (task: Task, storyId: StoryId) => void,
isRaised: boolean,
index: number,
storyId: StoryId
}
Expand Down Expand Up @@ -38,10 +39,8 @@ export default class TaskItem extends React.Component<Props> {
}

toggleBody() {
this.setState({
...this.state,
isBodyVisible: !this.state.isBodyVisible
});
const isOpen = this.props.task.isBodyVisible;
this.update("isBodyVisible", !isOpen);
}

update(key: string, value: any) {
Expand Down Expand Up @@ -75,7 +74,7 @@ export default class TaskItem extends React.Component<Props> {
}

render() {
const { task, addTaskAtIndex, index } = this.props;
const { task, addTaskAtIndex, index, isRaised } = this.props;

const optionsMenu = (
<Menu>
Expand All @@ -84,7 +83,7 @@ export default class TaskItem extends React.Component<Props> {
);

return (
<div className="task-item">
<div className={`task-item ${isRaised ? 'task-item--raised' : ''}`}>
<AddTaskButton type="before" visible={index === 0} index={index} addTask={(index: number) => addTaskAtIndex(index)} />

<div className="task-item__header" onClick={this.toggleBody}>
Expand All @@ -105,7 +104,7 @@ export default class TaskItem extends React.Component<Props> {
</div>
</div>

<Collapse isOpen={this.state.isBodyVisible}>
<Collapse isOpen={task.isBodyVisible}>
<div className="task-item__body">
<TaskForm task={task} update={this.update} updateEffort={this.updateEffort} showDates={false} />
</div>
Expand Down
88 changes: 68 additions & 20 deletions src/components/TaskList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { IStore } from "../redux/reducers";
import { Task, TaskId, TaskType } from "../entities/task.entity";
import TaskItem from "./TaskItem";
import { NonIdealState, Button } from "@blueprintjs/core";
import { addTask, updateTask, removeTask } from "../redux/actions";
import { addTask, updateTask, removeTask, moveTask } from "../redux/actions";
import Notification from "../entities/notification.entity";
import { DragDropContext, Droppable, Draggable, DropResult } from "react-beautiful-dnd";

interface Props {
tasks: { [key: number]: Task },
Expand All @@ -22,6 +23,7 @@ class TaskList extends React.Component<Props> {
this.addTask = this.addTask.bind(this);
this.updateTask = this.updateTask.bind(this);
this.removeTask = this.removeTask.bind(this);
this.onDragEnd = this.onDragEnd.bind(this);
}

public addTask(type = TaskType.TASK, index = -1) {
Expand All @@ -38,31 +40,62 @@ class TaskList extends React.Component<Props> {
this.props.notify(new Notification(`"${task.title}" was deleted successfully.`, "tick"));
}

public onDragEnd(result: DropResult) {
// dropped outside the list
if (!result.destination) {
return;
}

const storyId = this.props.story.id;

this.props.dispatch(moveTask(
storyId,
result.source.index,
result.destination.index)
);
}

render() {
const { story } = this.props;
const hasAnyTask = story && story.tasks && story.tasks.length > 0;

return (
<div className="task-list">
<div className="task-list__header"></div>
{
story && story.tasks && story.tasks.length > 0 ?
story.tasks.map((taskId, i) => {
const task = this.props.tasks[taskId];
return <TaskItem
key={`task#${taskId}`}
index={i}
task={task}
storyId={story.id}
updateTask={this.updateTask}
addTaskAtIndex={(index) => this.addTask(TaskType.TASK, index)}
removeTask={this.removeTask} />
})
: <NonIdealState
className="story-details__nothing"
title="No tasks here 😴 (yet)"
icon="add"
action={<Button intent="success" text="Add a task" icon="add-to-artifact" onClick={() => this.addTask()} />} />
}

<div className="task-list__items">
{
hasAnyTask
? <DragDrop onDragEnd={this.onDragEnd}>
{
story.tasks.map((taskId, i) => {
const task = this.props.tasks[taskId];

return <Draggable key={`task#${taskId}`} draggableId={task.id} index={i}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<TaskItem
index={i}
task={task}
storyId={story.id}
updateTask={this.updateTask}
addTaskAtIndex={(index) => this.addTask(TaskType.TASK, index)}
removeTask={this.removeTask}
isRaised={snapshot.isDragging} />
</div>
)}
</Draggable>
})
}
</DragDrop>
: <NonIdealState
className="story-details__nothing"
title="No tasks here 😴 (yet)"
icon="add"
action={<Button intent="success" text="Add a task" icon="add-to-artifact" onClick={() => this.addTask()} />} />
}
</div>

</div>
);
}
Expand All @@ -76,4 +109,19 @@ const mapStateToProps = (state: IStore, props: any): Props => {
};
}

const DragDrop = (props: any) => {
return (
<DragDropContext onDragEnd={props.onDragEnd}>
<Droppable droppableId="droppable">
{(provided, snapshot) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{props.children}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};

export default connect(mapStateToProps)(TaskList);
4 changes: 3 additions & 1 deletion src/entities/task.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export class Task {
this.handOffDate = new Date();
this.area = TaskArea.BOTH;
this.description = "";
this.assignee = Assignee.A1
this.assignee = Assignee.A1;
this.isBodyVisible = true;
}

public id: TaskId;
Expand All @@ -47,4 +48,5 @@ export class Task {
public description: string;
public area: TaskArea;
public assignee: string;
public isBodyVisible: boolean;
}
8 changes: 8 additions & 0 deletions src/redux/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export enum ActionType {
// TASK
ADD_TASK,
UPDATE_TASK,
MOVE_TASK,
REMOVE_TASK
}

Expand Down Expand Up @@ -176,6 +177,13 @@ export function updateTask(task: Task): IAction {
};
}

export function moveTask(storyId: StoryId, from: number, to: number): IAction {
return {
type: ActionType.MOVE_TASK,
payload: { storyId, from, to }
};
}

export function removeTask(taskId: TaskId, storyId: StoryId): IAction {
return {
type: ActionType.REMOVE_TASK,
Expand Down
19 changes: 18 additions & 1 deletion src/redux/reducers/stories.reducer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ActionType } from "../actions";
import IEntityMap from "../utils/entity-map.interface";
import IAction from "../utils/action.interface";
import { removeFromArray, removeFromObject, addToArray } from "../utils/store.utils";
import { removeFromArray, removeFromObject, addToArray, moveInArray } from "../utils/store.utils";
import { Story } from "../../entities/story.entity";

export default function storiesReducer(
Expand All @@ -21,6 +21,9 @@ export default function storiesReducer(
case ActionType.ADD_TASK:
return addTaskToStory(state, action.payload);

case ActionType.MOVE_TASK:
return moveTaskInStory(state, action.payload);

case ActionType.REMOVE_TASK:
return removeTaskFromStory(state, action.payload);

Expand Down Expand Up @@ -78,6 +81,20 @@ const addTaskToStory = (state: IEntityMap<Story>, payload: any) => {
};
}

const moveTaskInStory = (state: IEntityMap<Story>, payload: any) => {
const { storyId, from, to } = payload;
const story = { ...state.entities[storyId] };
story.tasks = moveInArray(story.tasks, from, to);

return {
...state,
entities: {
...state.entities,
[storyId]: story
},
};
}

const removeTaskFromStory = (state: IEntityMap<Story>, payload: any) => {
const { taskId, storyId } = payload;
const story = state.entities[storyId];
Expand Down
8 changes: 8 additions & 0 deletions src/redux/utils/store.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export const addToArray = (arr: any[], el: number | string, position: number): a
}
}

export const moveInArray = (arr: any[], from: number, to: number): any => {
const clone = Array.from(arr);
const [item] = clone.splice(from, 1);
clone.splice(to, 0, item);

return clone;
}

export const removeFromArray = (arr: any[], el: number | string): any => {
let clone = [...arr];
clone.splice(clone.indexOf(el), 1);
Expand Down
1 change: 1 addition & 0 deletions src/services/trader.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class Trader {
task.type = taskObj.type;
task.area = taskObj.area;
task.assignee = taskObj.assignee;
task.isBodyVisible = taskObj.isBodyVisible === undefined ? true : taskObj.isBodyVisible;

return task;
} catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion src/styles/components/story-details.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

&__body {
margin-bottom: 12px;
margin-top: -8px;
margin-top: -6px;
}

&__nothing {
Expand Down
2 changes: 1 addition & 1 deletion src/styles/components/story-form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
textarea {
resize: vertical;
font-family: 'Fira Code';
min-height: 100px;
min-height: 200px;
}

.bp3-numeric-input input {
Expand Down
15 changes: 5 additions & 10 deletions src/styles/components/task-item.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
border-radius: 3px;
padding: 8px;
position: relative;
animation: 3s 1 alternate task-border;
transition: all .2s ease-in-out;

&__header {
padding: 10px;
Expand Down Expand Up @@ -93,14 +93,9 @@
width: 76;
}
}
}

@keyframes task-border {
from {
box-shadow: 0px 0px 5px #756dfd;
}

to {
box-shadow: 0px 0px 3px rgba(0,0,0,0.2);
&--raised {
transform: scale(1.02);
box-shadow: 0px 7px 7px rgba(0, 0, 0, 0.2);
}
}
}
8 changes: 4 additions & 4 deletions src/styles/components/task-list.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
.task-list {
margin-top: 6px;

.task-item + .task-item {
margin-top: 16px;
}

&__header {
text-align: right;
}

&__items > div > div {
margin-top: 16px;
}
}
2 changes: 1 addition & 1 deletion src/styles/root.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ button svg[data-icon*="chevron"] {
}
}

[class*="bp3-"]:focus {
:focus, [class*="bp3-"]:focus {
outline: none;
box-shadow: 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2)
}
Expand Down
Loading

0 comments on commit 5579c34

Please sign in to comment.