Feature Request: Ability to set task status via context menu #416
This commit is contained in:
parent
ac5e60ffbc
commit
9e4f0860dc
|
|
@ -108,6 +108,7 @@ class EntityAction extends EnumClass {
|
||||||
static const EntityAction resendInvite = _$resendInvite;
|
static const EntityAction resendInvite = _$resendInvite;
|
||||||
static const EntityAction disconnect = _$disconnect;
|
static const EntityAction disconnect = _$disconnect;
|
||||||
static const EntityAction viewInvoice = _$viewInvoice;
|
static const EntityAction viewInvoice = _$viewInvoice;
|
||||||
|
static const EntityAction changeStatus = _$changeStatus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ const EntityAction _$invoiceProject = const EntityAction._('invoiceProject');
|
||||||
const EntityAction _$resendInvite = const EntityAction._('resendInvite');
|
const EntityAction _$resendInvite = const EntityAction._('resendInvite');
|
||||||
const EntityAction _$disconnect = const EntityAction._('disconnect');
|
const EntityAction _$disconnect = const EntityAction._('disconnect');
|
||||||
const EntityAction _$viewInvoice = const EntityAction._('viewInvoice');
|
const EntityAction _$viewInvoice = const EntityAction._('viewInvoice');
|
||||||
|
const EntityAction _$changeStatus = const EntityAction._('changeStatus');
|
||||||
|
|
||||||
EntityAction _$valueOf(String name) {
|
EntityAction _$valueOf(String name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
|
@ -199,6 +200,8 @@ EntityAction _$valueOf(String name) {
|
||||||
return _$disconnect;
|
return _$disconnect;
|
||||||
case 'viewInvoice':
|
case 'viewInvoice':
|
||||||
return _$viewInvoice;
|
return _$viewInvoice;
|
||||||
|
case 'changeStatus':
|
||||||
|
return _$changeStatus;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentError(name);
|
throw new ArgumentError(name);
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +270,7 @@ final BuiltSet<EntityAction> _$values =
|
||||||
_$resendInvite,
|
_$resendInvite,
|
||||||
_$disconnect,
|
_$disconnect,
|
||||||
_$viewInvoice,
|
_$viewInvoice,
|
||||||
|
_$changeStatus,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Serializer<EntityAction> _$entityActionSerializer =
|
Serializer<EntityAction> _$entityActionSerializer =
|
||||||
|
|
|
||||||
|
|
@ -596,7 +596,15 @@ abstract class TaskEntity extends Object
|
||||||
} else {
|
} else {
|
||||||
actions.add(EntityAction.start);
|
actions.add(EntityAction.start);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!multiselect && isOld) {
|
||||||
|
if (userCompany.canEditEntity(this)) {
|
||||||
|
actions.add(EntityAction.changeStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInvoiced && !isRunning) {
|
||||||
if (userCompany.canCreate(EntityType.invoice)) {
|
if (userCompany.canCreate(EntityType.invoice)) {
|
||||||
actions.add(EntityAction.invoiceTask);
|
actions.add(EntityAction.invoiceTask);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import 'package:invoiceninja_flutter/redux/document/document_actions.dart';
|
||||||
import 'package:invoiceninja_flutter/redux/task/task_selectors.dart';
|
import 'package:invoiceninja_flutter/redux/task/task_selectors.dart';
|
||||||
import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart';
|
import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart';
|
||||||
import 'package:invoiceninja_flutter/utils/completers.dart';
|
import 'package:invoiceninja_flutter/utils/completers.dart';
|
||||||
|
import 'package:invoiceninja_flutter/utils/dialogs.dart';
|
||||||
import 'package:invoiceninja_flutter/utils/formatting.dart';
|
import 'package:invoiceninja_flutter/utils/formatting.dart';
|
||||||
import 'package:invoiceninja_flutter/utils/localization.dart';
|
import 'package:invoiceninja_flutter/utils/localization.dart';
|
||||||
|
|
||||||
|
|
@ -442,6 +443,9 @@ void handleTaskAction(
|
||||||
case EntityAction.clone:
|
case EntityAction.clone:
|
||||||
createEntity(context: context, entity: task.clone);
|
createEntity(context: context, entity: task.clone);
|
||||||
break;
|
break;
|
||||||
|
case EntityAction.changeStatus:
|
||||||
|
changeTaskStatusDialog(context: context, task: task);
|
||||||
|
break;
|
||||||
case EntityAction.restore:
|
case EntityAction.restore:
|
||||||
final message = taskIds.length > 1
|
final message = taskIds.length > 1
|
||||||
? localization.restoredTasks
|
? localization.restoredTasks
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,39 @@ import 'package:invoiceninja_flutter/redux/app/app_state.dart';
|
||||||
import 'package:invoiceninja_flutter/redux/static/static_state.dart';
|
import 'package:invoiceninja_flutter/redux/static/static_state.dart';
|
||||||
import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart';
|
import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart';
|
||||||
|
|
||||||
|
var memoizedSortedActiveTaskStatusIds = memo2((
|
||||||
|
BuiltList<String> taskStatusList,
|
||||||
|
BuiltMap<String, TaskStatusEntity> taskStatusMap,
|
||||||
|
) =>
|
||||||
|
sortedActiveTaskStatusIds(
|
||||||
|
taskStatusList: taskStatusList,
|
||||||
|
taskStatusMap: taskStatusMap,
|
||||||
|
));
|
||||||
|
|
||||||
|
List<String> sortedActiveTaskStatusIds({
|
||||||
|
BuiltMap<String, TaskStatusEntity> taskStatusMap,
|
||||||
|
BuiltList<String> taskStatusList,
|
||||||
|
}) {
|
||||||
|
final statuses = taskStatusList
|
||||||
|
.where((statusId) =>
|
||||||
|
taskStatusMap.containsKey(statusId) &&
|
||||||
|
taskStatusMap[statusId].isActive)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
statuses.sort((statusIdA, statusIdB) {
|
||||||
|
final statusA = taskStatusMap[statusIdA];
|
||||||
|
final statusB = taskStatusMap[statusIdB];
|
||||||
|
if (statusA.statusOrder == statusB.statusOrder) {
|
||||||
|
return statusB.updatedAt.compareTo(statusA.updatedAt);
|
||||||
|
} else {
|
||||||
|
return (statusA.statusOrder ?? 99999)
|
||||||
|
.compareTo(statusB.statusOrder ?? 99999);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return statuses;
|
||||||
|
}
|
||||||
|
|
||||||
var memoizedDropdownTaskStatusList = memo4(
|
var memoizedDropdownTaskStatusList = memo4(
|
||||||
(BuiltMap<String, TaskStatusEntity> taskStatusMap,
|
(BuiltMap<String, TaskStatusEntity> taskStatusMap,
|
||||||
BuiltList<String> taskStatusList,
|
BuiltList<String> taskStatusList,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import 'package:boardview/boardview_controller.dart';
|
||||||
|
|
||||||
// Project imports:
|
// Project imports:
|
||||||
import 'package:invoiceninja_flutter/data/models/models.dart';
|
import 'package:invoiceninja_flutter/data/models/models.dart';
|
||||||
|
import 'package:invoiceninja_flutter/redux/task_status/task_status_selectors.dart';
|
||||||
import 'package:invoiceninja_flutter/ui/task/kanban/kanban_card.dart';
|
import 'package:invoiceninja_flutter/ui/task/kanban/kanban_card.dart';
|
||||||
import 'package:invoiceninja_flutter/ui/task/kanban/kanban_status.dart';
|
import 'package:invoiceninja_flutter/ui/task/kanban/kanban_status.dart';
|
||||||
import 'package:invoiceninja_flutter/ui/task/kanban/kanban_view_vm.dart';
|
import 'package:invoiceninja_flutter/ui/task/kanban/kanban_view_vm.dart';
|
||||||
|
|
@ -52,20 +53,8 @@ class KanbanViewState extends State<KanbanView> {
|
||||||
final viewModel = widget.viewModel;
|
final viewModel = widget.viewModel;
|
||||||
final state = viewModel.state;
|
final state = viewModel.state;
|
||||||
|
|
||||||
_statuses = state.taskStatusState.list
|
_statuses = memoizedSortedActiveTaskStatusIds(
|
||||||
.where((statusId) => state.taskStatusState.get(statusId).isActive)
|
state.taskStatusState.list, state.taskStatusState.map);
|
||||||
.toList();
|
|
||||||
|
|
||||||
_statuses.sort((statusIdA, statusIdB) {
|
|
||||||
final statusA = state.taskStatusState.get(statusIdA);
|
|
||||||
final statusB = state.taskStatusState.get(statusIdB);
|
|
||||||
if (statusA.statusOrder == statusB.statusOrder) {
|
|
||||||
return statusB.updatedAt.compareTo(statusA.updatedAt);
|
|
||||||
} else {
|
|
||||||
return (statusA.statusOrder ?? 99999)
|
|
||||||
.compareTo(statusB.statusOrder ?? 99999);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_tasks = {};
|
_tasks = {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import 'package:flutter/services.dart';
|
||||||
|
|
||||||
// Package imports:
|
// Package imports:
|
||||||
import 'package:flutter_redux/flutter_redux.dart';
|
import 'package:flutter_redux/flutter_redux.dart';
|
||||||
|
import 'package:invoiceninja_flutter/redux/task/task_actions.dart';
|
||||||
|
import 'package:invoiceninja_flutter/redux/task_status/task_status_selectors.dart';
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||||
|
|
||||||
// Project imports:
|
// Project imports:
|
||||||
|
|
@ -447,3 +449,50 @@ void cloneToDialog({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void changeTaskStatusDialog({
|
||||||
|
@required BuildContext context,
|
||||||
|
@required TaskEntity task,
|
||||||
|
}) {
|
||||||
|
final localization = AppLocalization.of(context);
|
||||||
|
final store = StoreProvider.of<AppState>(context);
|
||||||
|
final state = store.state;
|
||||||
|
final statusIds = memoizedSortedActiveTaskStatusIds(
|
||||||
|
state.taskStatusState.list, state.taskStatusState.map)
|
||||||
|
.where((statusId) => statusId != task.statusId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
showDialog<AlertDialog>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(localization.changeStatus),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: statusIds.map((statusId) {
|
||||||
|
final status = state.taskStatusState.get(statusId);
|
||||||
|
return ListTile(
|
||||||
|
title: Text(status.name),
|
||||||
|
leading: Icon(Icons.check_circle),
|
||||||
|
onTap: () {
|
||||||
|
store.dispatch(SaveTaskRequest(
|
||||||
|
task: task.rebuild((b) => b..statusId = statusId),
|
||||||
|
completer: snackBarCompleter<TaskEntity>(
|
||||||
|
context,
|
||||||
|
localization.changedStatus,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: Text(localization.close.toUpperCase()),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ mixin LocalizationsProvider on LocaleCodeAware {
|
||||||
static final Map<String, Map<String, String>> _localizedValues = {
|
static final Map<String, Map<String, String>> _localizedValues = {
|
||||||
'en': {
|
'en': {
|
||||||
// STARTER: lang key - do not remove comment
|
// STARTER: lang key - do not remove comment
|
||||||
|
'changed_status': 'Successfully changed task status',
|
||||||
|
'change_status': 'Change Status',
|
||||||
'fees_sample': 'The fee for a :amount invoice would be :total.',
|
'fees_sample': 'The fee for a :amount invoice would be :total.',
|
||||||
'enable_touch_events': 'Enable Touch Events',
|
'enable_touch_events': 'Enable Touch Events',
|
||||||
'enable_touch_events_help': 'Support drag events to scroll',
|
'enable_touch_events_help': 'Support drag events to scroll',
|
||||||
|
|
@ -74215,6 +74217,14 @@ mixin LocalizationsProvider on LocaleCodeAware {
|
||||||
_localizedValues[localeCode]['fees_sample'] ??
|
_localizedValues[localeCode]['fees_sample'] ??
|
||||||
_localizedValues['en']['fees_sample'];
|
_localizedValues['en']['fees_sample'];
|
||||||
|
|
||||||
|
String get changeStatus =>
|
||||||
|
_localizedValues[localeCode]['change_status'] ??
|
||||||
|
_localizedValues['en']['change_status'];
|
||||||
|
|
||||||
|
String get changedStatus =>
|
||||||
|
_localizedValues[localeCode]['changed_status'] ??
|
||||||
|
_localizedValues['en']['changed_status'];
|
||||||
|
|
||||||
// STARTER: lang field - do not remove comment
|
// STARTER: lang field - do not remove comment
|
||||||
|
|
||||||
String lookup(String key) {
|
String lookup(String key) {
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,8 @@ IconData getEntityActionIcon(EntityAction entityAction) {
|
||||||
return Icons.delete_forever;
|
return Icons.delete_forever;
|
||||||
case EntityAction.viewInvoice:
|
case EntityAction.viewInvoice:
|
||||||
return MdiIcons.fileAccount;
|
return MdiIcons.fileAccount;
|
||||||
|
case EntityAction.changeStatus:
|
||||||
|
return Icons.adjust;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue