diff --git a/lib/src/models/work_item_type_rules.dart b/lib/src/models/work_item_type_rules.dart index f180a642..c3ba261c 100644 --- a/lib/src/models/work_item_type_rules.dart +++ b/lib/src/models/work_item_type_rules.dart @@ -46,15 +46,18 @@ class Action { Action({ required this.actionType, required this.targetField, + this.value, }); factory Action.fromJson(Map json) => Action( actionType: ActionType.fromString(json['actionType']?.toString() ?? ''), targetField: json['targetField'] as String, + value: json['value'] as String?, ); final ActionType actionType; final String targetField; + final String? value; @override String toString() => 'Action(actionType: $actionType, targetField: $targetField)'; @@ -64,6 +67,7 @@ enum ActionType { makeReadOnly, makeRequired, setValueToEmpty, + disallowValue, notSupported; static ActionType fromString(String str) { @@ -74,6 +78,8 @@ enum ActionType { return ActionType.makeRequired; case 'setValueToEmpty': return ActionType.setValueToEmpty; + case 'disallowValue': + return ActionType.disallowValue; default: return ActionType.notSupported; } diff --git a/lib/src/screens/create_or_edit_work_item/controller_create_or_edit_work_item.dart b/lib/src/screens/create_or_edit_work_item/controller_create_or_edit_work_item.dart index b28b276e..f4e6f511 100644 --- a/lib/src/screens/create_or_edit_work_item/controller_create_or_edit_work_item.dart +++ b/lib/src/screens/create_or_edit_work_item/controller_create_or_edit_work_item.dart @@ -73,7 +73,7 @@ class _CreateOrEditWorkItemController with FilterMixin, AppLogger { } Future init() async { - if (args.id != null) { + if (isEditing) { // edit existent work item final res = await apiService.getWorkItemDetail(projectName: args.project!, workItemId: args.id!); if (!res.isError) { @@ -85,19 +85,6 @@ class _CreateOrEditWorkItemController with FilterMixin, AppLogger { if (!types.isError) { allWorkItemTypes.addAll(types.data!.values.expand((ts) => ts).toSet()); allProjectsWorkItemTypes = types.data!; - - if (!isEditing) { - final allStatesToAdd = {}; - - for (final entry in apiService.workItemStates.values) { - final states = entry.values.expand((v) => v); - allStatesToAdd.addAll(states); - } - - final sortedStates = allStatesToAdd.sorted((a, b) => a.name.compareTo(b.name)); - - allWorkItemStates.addAll(sortedStates); - } } if (newWorkItemType != WorkItemType.all) await _getTypeFormFields(); @@ -330,6 +317,13 @@ class _CreateOrEditWorkItemController with FilterMixin, AppLogger { ..popupMenuKey = field.value.popupMenuKey ..text = field.value.text, }; + + if (isEditing) { + allWorkItemStates = + _getTransitionableStates(project: newWorkItemProject.name!, workItemType: newWorkItemType.name); + } + + _checkRules(); } String? _checkRequiredFields() { @@ -513,6 +507,12 @@ class _CreateOrEditWorkItemController with FilterMixin, AppLogger { } } } + + final disallowedStates = checker.getDisallowedStates(); + + if (disallowedStates.isNotEmpty) { + allWorkItemStates.removeWhere((s) => disallowedStates.contains(s.name)); + } } void onFieldChanged(String str, String fieldRefName, {bool checkRules = true}) { diff --git a/lib/src/services/azure_api_service.dart b/lib/src/services/azure_api_service.dart index a8337699..124e473c 100644 --- a/lib/src/services/azure_api_service.dart +++ b/lib/src/services/azure_api_service.dart @@ -46,7 +46,7 @@ import 'package:http/http.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:xml/xml.dart'; -typedef WorkItemRule = ({ActionType action, List conditions}); +typedef WorkItemRule = ({Action action, List conditions}); typedef WorkItemTypeRules = Map>; @@ -986,10 +986,12 @@ class AzureApiServiceImpl with AppLogger implements AzureApiService { final isVisibleField = actions.any((a) => fieldNames.contains(a.targetField)); final isSupportedAction = actions.any((a) => !_fieldNamesToSkip.contains(a.targetField)); + final isStateRule = actions.any((a) => 'System.State' == a.targetField); + for (final action in actions) { - if (isVisibleField && isSupportedAction) { + if (isStateRule || (isVisibleField && isSupportedAction)) { mappedRules.putIfAbsent(action.targetField, () => []); - mappedRules[action.targetField]!.add((action: action.actionType, conditions: conditions)); + mappedRules[action.targetField]!.add((action: action, conditions: conditions)); } } } diff --git a/lib/src/services/rules_checker.dart b/lib/src/services/rules_checker.dart index fd7b60f9..bdc1e6c8 100644 --- a/lib/src/services/rules_checker.dart +++ b/lib/src/services/rules_checker.dart @@ -40,12 +40,19 @@ class RulesChecker { return (readOnly: readOnly, required: required, makeEmpty: makeEmpty); } + /// Returns the list of states that are disallowed according to the rules. + /// This handles the 'Restrict the transition to state' rule. + List getDisallowedStates() { + final field = WorkItemField(referenceName: 'System.State', name: 'State'); + return _getDisallowedStates(field); + } + /// Checks whether this field should be read-only according to the rules. bool _checkIfIsReadOnly(WorkItemField field) { final rules = allRules[field.referenceName] ?? []; if (rules.isEmpty) return false; - final makeReadOnlyActions = rules.where((r) => r.action == ActionType.makeReadOnly).toList(); + final makeReadOnlyActions = rules.where((r) => r.action.actionType == ActionType.makeReadOnly).toList(); if (makeReadOnlyActions.isEmpty) return false; return _checkIfMatchesRule(makeReadOnlyActions); @@ -56,7 +63,7 @@ class RulesChecker { final rules = allRules[field.referenceName] ?? []; if (rules.isEmpty) return false; - final makeRequiredActions = rules.where((r) => r.action == ActionType.makeRequired).toList(); + final makeRequiredActions = rules.where((r) => r.action.actionType == ActionType.makeRequired).toList(); if (makeRequiredActions.isEmpty) return false; return _checkIfMatchesRule(makeRequiredActions); @@ -67,15 +74,33 @@ class RulesChecker { final rules = allRules[field.referenceName] ?? []; if (rules.isEmpty) return false; - final makeEmptyActions = rules.where((r) => r.action == ActionType.setValueToEmpty).toList(); + final makeEmptyActions = rules.where((r) => r.action.actionType == ActionType.setValueToEmpty).toList(); if (makeEmptyActions.isEmpty) return false; return _checkIfMatchesRule(makeEmptyActions); } + List _getDisallowedStates(WorkItemField field) { + final rules = allRules[field.referenceName] ?? []; + if (rules.isEmpty) return []; + + final disallowValueActions = rules.where((r) => r.action.actionType == ActionType.disallowValue).toList(); + if (disallowValueActions.isEmpty) return []; + + final disallowedStates = []; + + for (final action in disallowValueActions) { + final match = _checkIfMatchesRule([action]); + final targetState = action.action.value; + if (match && targetState != null) disallowedStates.add(targetState); + } + + return disallowedStates; + } + /// Checks if any of the [rules] should be applied bool _checkIfMatchesRule(List rules) { - var isReadOnly = false; + var matched = false; for (final rule in rules) { final conditions = rule.conditions; @@ -83,17 +108,17 @@ class RulesChecker { if (conditions.length == 1) { final cond = conditions.single; - isReadOnly |= _checkSingleRule(cond); + matched |= _checkSingleRule(cond); continue; } // we have 2 conditions final firstCond = conditions.first; final secondCond = conditions.last; - isReadOnly |= _checkSingleRule(firstCond) && _checkSingleRule(secondCond); + matched |= _checkSingleRule(firstCond) && _checkSingleRule(secondCond); } - return isReadOnly; + return matched; } bool _checkSingleRule(Condition cond) { diff --git a/test/rules_checker_test.dart b/test/rules_checker_test.dart index 4ade1583..20311420 100644 --- a/test/rules_checker_test.dart +++ b/test/rules_checker_test.dart @@ -14,7 +14,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onCreate()], ), ], @@ -36,7 +36,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onCreate()], ), ], @@ -58,7 +58,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onCreate()], ), ], @@ -83,13 +83,13 @@ void main() { allRules: { requiredFieldName: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onCreate()], ), ], readOnlyFieldName: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onCreate()], ), ], @@ -120,11 +120,11 @@ void main() { allRules: { requiredFieldName: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onCreate()], ), ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onCreate()], ), ], @@ -151,7 +151,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onStateChanged()], ), ], @@ -176,7 +176,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onStateChanged()], ), ], @@ -201,7 +201,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onStateChanged()], ), ], @@ -227,7 +227,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onStateNotChanged()], ), ], @@ -251,7 +251,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onStateNotChanged()], ), ], @@ -275,7 +275,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onStateNotChanged()], ), ], @@ -302,7 +302,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onStateChangedFrom(previousState)], ), ], @@ -327,7 +327,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onStateChangedFrom(previousState)], ), ], @@ -352,7 +352,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onStateChangedFrom(previousState)], ), ], @@ -379,7 +379,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onStateChangedTo(currentState)], ), ], @@ -404,7 +404,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onStateChangedTo(currentState)], ), ], @@ -429,7 +429,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onStateChangedTo(currentState)], ), ], @@ -456,7 +456,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onStateIsNot(stateToCheck)], ), ], @@ -481,7 +481,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onStateIsNot(stateToCheck)], ), ], @@ -506,7 +506,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onStateIsNot(stateToCheck)], ), ], @@ -533,7 +533,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onFieldValueIs(fieldNameWithValue, value)], ), ], @@ -561,7 +561,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onFieldValueIs(fieldNameWithValue, value)], ), ], @@ -589,7 +589,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onFieldValueIs(fieldNameWithValue, value)], ), ], @@ -620,7 +620,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onFieldValueIsNot(fieldNameWithValue, initialValue)], ), ], @@ -649,7 +649,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onFieldValueIsNot(fieldNameWithValue, initialValue)], ), ], @@ -678,7 +678,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onFieldValueIsNot(fieldNameWithValue, initialValue)], ), ], @@ -707,7 +707,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onFieldValueChanged(changedFieldName)], ), ], @@ -734,7 +734,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onFieldValueChanged(changedFieldName)], ), ], @@ -761,7 +761,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onFieldValueChanged(changedFieldName)], ), ], @@ -791,7 +791,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeRequired, + action: ActionCreator.fromActionType(ActionType.makeRequired), conditions: [ConditionCreator.onFieldValueNotChanged(notChangedFieldName)], ), ], @@ -819,7 +819,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.makeReadOnly, + action: ActionCreator.fromActionType(ActionType.makeReadOnly), conditions: [ConditionCreator.onFieldValueNotChanged(notChangedFieldName)], ), ], @@ -847,7 +847,7 @@ void main() { allRules: { _fieldNameToCheck: [ ( - action: ActionType.setValueToEmpty, + action: ActionCreator.fromActionType(ActionType.setValueToEmpty), conditions: [ConditionCreator.onFieldValueNotChanged(notChangedFieldName)], ), ], @@ -917,3 +917,7 @@ class ConditionCreator { static Condition onFieldValueNotChanged(String fieldName) => Condition(conditionType: ConditionType.whenNotChanged, field: fieldName, value: null); } + +class ActionCreator { + static Action fromActionType(ActionType actionType) => Action(actionType: actionType, targetField: ''); +}