Skip to content

Commit

Permalink
rules_checker: handle disallowed states
Browse files Browse the repository at this point in the history
This handles the 'Restrict the transition to state' rule
  • Loading branch information
sstasi95 committed Dec 11, 2023
1 parent 2bfde4d commit 0fc8751
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 58 deletions.
6 changes: 6 additions & 0 deletions lib/src/models/work_item_type_rules.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,18 @@ class Action {
Action({
required this.actionType,
required this.targetField,
this.value,
});

factory Action.fromJson(Map<String, dynamic> 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)';
Expand All @@ -64,6 +67,7 @@ enum ActionType {
makeReadOnly,
makeRequired,
setValueToEmpty,
disallowValue,
notSupported;

static ActionType fromString(String str) {
Expand All @@ -74,6 +78,8 @@ enum ActionType {
return ActionType.makeRequired;
case 'setValueToEmpty':
return ActionType.setValueToEmpty;
case 'disallowValue':
return ActionType.disallowValue;
default:
return ActionType.notSupported;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class _CreateOrEditWorkItemController with FilterMixin, AppLogger {
}

Future<void> 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) {
Expand All @@ -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 = <WorkItemState>{};

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();
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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}) {
Expand Down
8 changes: 5 additions & 3 deletions lib/src/services/azure_api_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Condition> conditions});
typedef WorkItemRule = ({Action action, List<Condition> conditions});

typedef WorkItemTypeRules = Map<String, List<WorkItemRule>>;

Expand Down Expand Up @@ -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));
}
}
}
Expand Down
39 changes: 32 additions & 7 deletions lib/src/services/rules_checker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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);
Expand All @@ -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);
Expand All @@ -67,33 +74,51 @@ 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<String> _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 = <String>[];

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<WorkItemRule> rules) {
var isReadOnly = false;
var matched = false;

for (final rule in rules) {
final conditions = rule.conditions;
if (conditions.isEmpty) break;

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) {
Expand Down
Loading

0 comments on commit 0fc8751

Please sign in to comment.