Skip to content

Commit

Permalink
boards/sprints: add search items field
Browse files Browse the repository at this point in the history
Items can be searched by id or title.
  • Loading branch information
sstasi95 committed Feb 11, 2025
1 parent 5173d43 commit 430272d
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 10 deletions.
1 change: 1 addition & 0 deletions lib/src/screens/board_detail/base_board_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:azure_devops/src/widgets/board_widget.dart';
import 'package:azure_devops/src/widgets/filter_menu.dart';
import 'package:azure_devops/src/widgets/navigation_button.dart';
import 'package:azure_devops/src/widgets/popup_menu.dart';
import 'package:azure_devops/src/widgets/search_field.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

Expand Down
32 changes: 32 additions & 0 deletions lib/src/screens/board_detail/components_board_detail.dart
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
part of board_detail;

class _Actions extends StatelessWidget {
const _Actions({required this.ctrl});

final _BoardDetailController ctrl;

@override
Widget build(BuildContext context) {
return Flexible(
child: DevOpsAnimatedSearchField(
isSearching: ctrl.isSearching,
onChanged: ctrl._searchWorkItem,
onResetSearch: ctrl.resetSearch,
hint: 'Search by id or title',
margin: const EdgeInsets.only(left: 56, right: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SearchButton(
isSearching: ctrl.isSearching,
),
IconButton(
icon: const Icon(DevOpsIcons.plus),
onPressed: ctrl.addNewItem,
iconSize: 24,
),
],
),
),
);
}
}
35 changes: 34 additions & 1 deletion lib/src/screens/board_detail/controller_board_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class _BoardDetailController with ApiErrorHelper, AdsMixin, FilterMixin {

Set<String> get _allowedTypes => _data?.board.columns.firstOrNull?.stateMappings.keys.toSet() ?? {};

final isSearching = ValueNotifier<bool>(false);
String? _currentSearchQuery;

Future<void> init() async {
final res = await api.getProjectBoard(projectName: args.project, teamId: args.teamId, backlogId: args.backlogId);
_data = res.data;
Expand Down Expand Up @@ -55,7 +58,17 @@ class _BoardDetailController with ApiErrorHelper, AdsMixin, FilterMixin {
)
.toList();

columnItems[column]!.addAll(filteredByUsers);
final matchedItems = (_currentSearchQuery ?? '').isEmpty
? filteredByUsers
: filteredByUsers
.where(
(i) =>
i.id.toString().contains(_currentSearchQuery!) ||
i.fields.systemTitle.toLowerCase().contains(_currentSearchQuery!),
)
.toList();

columnItems[column]!.addAll(matchedItems);
}
}

Expand Down Expand Up @@ -189,6 +202,26 @@ class _BoardDetailController with ApiErrorHelper, AdsMixin, FilterMixin {
typesFilter.clear();
usersFilter.clear();

_currentSearchQuery = null;
_hideSearchField();

init();
}

/// Search currently filtered work items by id or title.
void _searchWorkItem(String query) {
_currentSearchQuery = query.trim().toLowerCase();

_fillColumns();
_refreshUI();
}

void resetSearch() {
_searchWorkItem('');
_hideSearchField();
}

void _hideSearchField() {
isSearching.value = false;
}
}
5 changes: 1 addition & 4 deletions lib/src/screens/board_detail/screen_board_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ class _BoardDetailScreen extends StatelessWidget {
notifier: ctrl.boardWithItems,
padding: EdgeInsets.zero,
actions: [
IconButton(
icon: const Icon(DevOpsIcons.plus),
onPressed: ctrl.addNewItem,
),
_Actions(ctrl: ctrl),
],
header: () => FiltersRow(
resetFilters: ctrl.resetFilters,
Expand Down
1 change: 1 addition & 0 deletions lib/src/screens/sprint_detail/base_sprint_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:azure_devops/src/widgets/app_page.dart';
import 'package:azure_devops/src/widgets/board_widget.dart';
import 'package:azure_devops/src/widgets/filter_menu.dart';
import 'package:azure_devops/src/widgets/popup_menu.dart';
import 'package:azure_devops/src/widgets/search_field.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

Expand Down
32 changes: 32 additions & 0 deletions lib/src/screens/sprint_detail/components_sprint_detail.dart
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
part of sprint_detail;

class _Actions extends StatelessWidget {
const _Actions({required this.ctrl});

final _SprintDetailController ctrl;

@override
Widget build(BuildContext context) {
return Flexible(
child: DevOpsAnimatedSearchField(
isSearching: ctrl.isSearching,
onChanged: ctrl._searchWorkItem,
onResetSearch: ctrl.resetSearch,
hint: 'Search by id or title',
margin: const EdgeInsets.only(left: 56, right: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SearchButton(
isSearching: ctrl.isSearching,
),
IconButton(
icon: const Icon(DevOpsIcons.plus),
onPressed: ctrl.addNewItem,
iconSize: 24,
),
],
),
),
);
}
}
35 changes: 34 additions & 1 deletion lib/src/screens/sprint_detail/controller_sprint_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class _SprintDetailController with FilterMixin {

SprintDetailWithItems? _data;

final isSearching = ValueNotifier<bool>(false);
String? _currentSearchQuery;

Future<void> init() async {
final res = await api.getProjectSprint(projectName: args.project, teamId: args.teamId, sprintId: args.sprintId);

Expand Down Expand Up @@ -55,7 +58,17 @@ class _SprintDetailController with FilterMixin {
)
.toList();

columnItems[column]!.addAll(filteredByUsers);
final matchedItems = (_currentSearchQuery ?? '').isEmpty
? filteredByUsers
: filteredByUsers
.where(
(i) =>
i.id.toString().contains(_currentSearchQuery!) ||
i.fields.systemTitle.toLowerCase().contains(_currentSearchQuery!),
)
.toList();

columnItems[column]!.addAll(matchedItems);
}
}

Expand Down Expand Up @@ -161,6 +174,26 @@ class _SprintDetailController with FilterMixin {
typesFilter.clear();
usersFilter.clear();

_currentSearchQuery = null;
_hideSearchField();

init();
}

/// Search currently filtered work items by id or title.
void _searchWorkItem(String query) {
_currentSearchQuery = query.trim().toLowerCase();

_fillColumns();
_refreshUI();
}

void resetSearch() {
_searchWorkItem('');
_hideSearchField();
}

void _hideSearchField() {
isSearching.value = false;
}
}
5 changes: 1 addition & 4 deletions lib/src/screens/sprint_detail/screen_sprint_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ class _SprintDetailScreen extends StatelessWidget {
notifier: ctrl.sprintWithItems,
padding: EdgeInsets.zero,
actions: [
IconButton(
icon: const Icon(DevOpsIcons.plus),
onPressed: ctrl.addNewItem,
),
_Actions(ctrl: ctrl),
],
header: () => FiltersRow(
resetFilters: ctrl.resetFilters,
Expand Down

0 comments on commit 430272d

Please sign in to comment.