Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3597 allow reordering tasks of a tour #3635

Merged
merged 13 commits into from
Jun 28, 2023
42 changes: 42 additions & 0 deletions features/dispatch.feature
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,48 @@ Feature: Dispatch
}
"""

Scenario: Add/reorder tasks of a tour
Given the fixtures files are loaded:
| dispatch.yml |
| tours.yml |
And the user "sarah" has role "ROLE_ADMIN"
And the user "sarah" is authenticated
When I add "Content-Type" header equal to "application/ld+json"
And I add "Accept" header equal to "application/ld+json"
And the user "sarah" sends a "PUT" request to "/api/tours/5" with body:
"""
{
"name":"Monday tour",
"tasks":[
"/api/tasks/3",
"/api/tasks/2",
"/api/tasks/1"
]
}
"""
Then the response status code should be 200
And the response should be in JSON
And the JSON should match:
"""
{
"@context":"/api/contexts/Tour",
"@id":"/api/tours/5",
"@type":"Tour",
"name":"Monday tour",
"items":[
"/api/tasks/3",
"/api/tasks/2",
"/api/tasks/1"
],
"distance":@integer@,
"duration":@integer@,
"polyline":@string@,
"createdAt":"@[email protected]()",
"updatedAt":"@[email protected]()"
}
"""


Scenario: Administrator can assign multiple tasks at once
Given the fixtures files are loaded:
| sylius_channels.yml |
Expand Down
7 changes: 7 additions & 0 deletions features/fixtures/ORM/tours.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
AppBundle\Entity\Tour:
tour_1:
name: Foo
tasks:
- '@task_1'
- '@task_2'
- '@task_3'
12 changes: 8 additions & 4 deletions features/tasks.feature
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Feature: Tasks
"createdAt":"@[email protected]()",
"tour":{
"@id":"/api/tours/1",
"name":"Example tour"
"name":"Example tour",
"position":@integer@
}
},
{
Expand Down Expand Up @@ -121,7 +122,8 @@ Feature: Tasks
"createdAt":"@[email protected]()",
"tour":{
"@id":"/api/tours/1",
"name":"Example tour"
"name":"Example tour",
"position":@integer@
}
},
{
Expand Down Expand Up @@ -489,7 +491,8 @@ Feature: Tasks
"createdAt":"@[email protected]()",
"tour":{
"@id":"/api/tours/1",
"name":"Example tour"
"name":"Example tour",
"position":@integer@
}
}
"""
Expand Down Expand Up @@ -2010,7 +2013,8 @@ Feature: Tasks
"status":"TODO",
"tour":{
"@id":"/api/tours/1",
"name":"Example tour"
"name":"Example tour",
"position":@integer@
},
"@*@":"@*@"
}
Expand Down
4 changes: 3 additions & 1 deletion js/app/dashboard/components/RightPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import UnassignedTasks from './UnassignedTasks'
import TaskLists from './TaskLists'
import ContextMenu from './ContextMenu'
import SearchPanel from './SearchPanel'
import UnassignedTours from './UnassignedTours'

class DashboardApp extends React.Component {

Expand All @@ -41,13 +42,14 @@ class DashboardApp extends React.Component {
onDragStart={ this.props.handleDragStart }
onDragEnd={ this.props.handleDragEnd }>
<Split
sizes={ [ 50, 50 ] }
sizes={ [ 33.33, 33.33, 33.33 ] }
direction={ this.props.splitDirection }
style={{ display: 'flex', flexDirection: this.props.splitDirection === 'vertical' ? 'column' : 'row', width: '100%' }}
// We need to use a "key" prop,
// to force a re-render when the direction has changed
key={ this.props.splitDirection }>
<UnassignedTasks />
<UnassignedTours />
<TaskLists couriersList={ this.props.couriersList } />
</Split>
</DragDropContext>
Expand Down
29 changes: 25 additions & 4 deletions js/app/dashboard/components/Task.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,18 @@ const TaskTags = ({ task }) => {
return null
}

const TaskIconRight = ({ task, assigned, onRemove }) => {
const TaskIconRight = ({ task, onRemove }) => {

const { t } = useTranslation()

if (assigned) {
if (task.isAssigned) {
switch (task.status) {
case 'TODO':

if (task.tour) {
return null
}

return (
<a
href="#"
Expand Down Expand Up @@ -114,6 +119,22 @@ const TaskIconRight = ({ task, assigned, onRemove }) => {
}
}

if (typeof onRemove === 'function') {

return (
<a
href="#"
className="task__icon task__icon--right"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onRemove(task)
}}
title={ t('ADMIN_DASHBOARD_UNASSIGN_TASK', { id: task.id }) }
><i className="fa fa-times"></i></a>
)
}

return null
}

Expand Down Expand Up @@ -154,7 +175,7 @@ class Task extends React.Component {

render() {

const { color, task, selected, isVisible, date, assigned } = this.props
const { color, task, selected, isVisible, date } = this.props

const classNames = [
'list-group-item',
Expand Down Expand Up @@ -210,7 +231,7 @@ class Task extends React.Component {
<TaskCaption task={ task } />
<TaskAttrs task={ task } />
<TaskTags task={ task } />
<TaskIconRight task={ task } assigned={ assigned } onRemove={ this.props.onRemove } />
<TaskIconRight task={ task } onRemove={ this.props.onRemove } />
<TaskEta
after={ task.after }
before={ task.before }
Expand Down
1 change: 0 additions & 1 deletion js/app/dashboard/components/TaskGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ class TaskGroup extends React.Component {
<Task
key={ task['@id'] }
task={ task }
assigned={ false }
/>
)
})}
Expand Down
2 changes: 0 additions & 2 deletions js/app/dashboard/components/TaskList.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ const TaskOrTour = ({ item, onRemove, unassignTasks, username }) => {
return (
<Task
task={ item }
assigned={ true }
onRemove={ item => onRemove(item) } />
)
}
Expand Down Expand Up @@ -70,7 +69,6 @@ class InnerList extends React.Component {
>
<TaskOrTour
item={ task }
assigned={ true }
onRemove={ task => this.props.onRemove(task) }
username={ this.props.username }
unassignTasks={ this.props.unassignTasks } />
Expand Down
1 change: 0 additions & 1 deletion js/app/dashboard/components/Tour.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const Tour = ({ tour, tasks, username = null, unassignTasks = null }) => {
<Task
key={ task['@id'] }
task={ task }
assigned={ false }
/>
)
})}
Expand Down
26 changes: 3 additions & 23 deletions js/app/dashboard/components/UnassignedTasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import { useTranslation } from 'react-i18next'

import Task from './Task'
import TaskGroup from './TaskGroup'
import Tour from './Tour'
import RecurrenceRule from './RecurrenceRule'
import UnassignedTasksPopoverContent from './UnassignedTasksPopoverContent'
import { setTaskListGroupMode, openNewTaskModal, toggleSearch, setCurrentRecurrenceRule, openNewRecurrenceRuleModal, deleteGroup, editGroup, showRecurrenceRules } from '../redux/actions'
import { selectGroups, selectStandaloneTasks, selectRecurrenceRules, selectSelectedTasks, selectTours } from '../redux/selectors'
import { selectGroups, selectStandaloneTasks, selectRecurrenceRules, selectSelectedTasks } from '../redux/selectors'

class StandaloneTasks extends React.Component {

Expand Down Expand Up @@ -163,28 +162,10 @@ class UnassignedTasks extends React.Component {
</Draggable>
)
})}
{ _.map(this.props.tours, (tour, index) => {
return (
<Draggable key={ `tour-${tour['@id']}` } draggableId={ `tour:${tour['@id']}` } index={ this.props.groups.length + index }>
{(provided) => (
<div
ref={ provided.innerRef }
{ ...provided.draggableProps }
{ ...provided.dragHandleProps }
>
<Tour
key={ tour['@id'] }
tour={ tour }
tasks={ tour.items }
/>
</div>
)}
</Draggable>
)
})}

<StandaloneTasksWithConnect
tasks={ this.props.standaloneTasks }
offset={ this.props.groups.length + this.props.tours.length } />
offset={ this.props.groups.length } />
{ provided.placeholder }
</div>
)}
Expand All @@ -199,7 +180,6 @@ function mapStateToProps (state) {

return {
groups: selectGroups(state),
tours: selectTours(state),
standaloneTasks: selectStandaloneTasks(state),
recurrenceRules: selectRecurrenceRules(state),
isRecurrenceRulesVisible: state.settings.isRecurrenceRulesVisible,
Expand Down
79 changes: 79 additions & 0 deletions js/app/dashboard/components/UnassignedTour.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react'
import { connect } from 'react-redux'
import { withTranslation, useTranslation } from 'react-i18next'
import { Draggable, Droppable } from "react-beautiful-dnd"
import _ from 'lodash'
import Task from './Task'
import { removeTaskFromTour } from '../redux/actions'

const UnassignedTour = ({ tour, tasks, removeTaskFromTour, username = null, unassignTasks = null, isDropDisabled }) => {

const { t } = useTranslation()

const collapseId = `tour-panel-${tour['@id'].replaceAll('/', '-')}`

return (
<div className="panel panel-default nomargin task__draggable">
<div className="panel-heading" role="tab">
<h4 className="panel-title d-flex align-items-center">
<i className="fa fa-repeat flex-grow-0"></i>
<a role="button" data-toggle="collapse" href={ `#${collapseId}` } className="ml-2 flex-grow-1 text-truncate">
{ tour.name } <span className="badge">{ tasks.length }</span>
</a>
{ username && (
<a
onClick={() => unassignTasks(username, tasks)}
title={ t('ADMIN_DASHBOARD_UNASSIGN_TOUR', { name: tour.name }) }
>
<i className="fa fa-times"></i>
</a>
)}
</h4>
</div>
<div id={ `${collapseId}` } className="panel-collapse collapse" role="tabpanel">
<Droppable isDropDisabled={isDropDisabled} droppableId={ `unassigned_tour:${tour['@id']}` }>
{(provided) => (
<div className="list-group list-group-padded nomargin taskList__tasks m-0" ref={ provided.innerRef } { ...provided.droppableProps }>
{ _.map(tasks, (task, index) => {
return (
<Draggable key={ `task-${task.id}` } draggableId={ `task:${task.id}` } index={ index }>
{(provided) => (
<div
ref={ provided.innerRef }
{ ...provided.draggableProps }
{ ...provided.dragHandleProps }
>
<Task
key={ task['@id'] }
task={ task }
onRemove={ (taskToRemove) => removeTaskFromTour(tour, taskToRemove) }
/>
</div>
)}
</Draggable>
)
})}
{ provided.placeholder }
</div>
)}
</Droppable>
</div>
</div>
)
}

function mapStateToProps (state) {

return {
isDropDisabled: state.logistics.ui.unassignedTourTasksDroppableDisabled,
}
}

function mapDispatchToProps(dispatch) {

return {
removeTaskFromTour: (tour, task) => dispatch(removeTaskFromTour(tour, task)),
}
}

export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(UnassignedTour))
Loading