Skip to content

Commit

Permalink
implement named filters
Browse files Browse the repository at this point in the history
- Add 'save filters' button that triggers an form
field to write filters label
- Show saved filters in home page
- Navigate to correct page with correct filters
by tapping a saved filter
  • Loading branch information
sstasi95 committed Jan 4, 2024
1 parent d46e3c2 commit c901b60
Show file tree
Hide file tree
Showing 24 changed files with 554 additions and 74 deletions.
19 changes: 11 additions & 8 deletions lib/src/router/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@ import 'package:azure_devops/src/screens/tabs/base_tabs.dart';
import 'package:azure_devops/src/screens/work_item_detail/base_work_item_detail.dart';
import 'package:azure_devops/src/screens/work_items/base_work_items.dart';
import 'package:azure_devops/src/services/overlay_service.dart';
import 'package:azure_devops/src/services/storage_service.dart';
import 'package:azure_devops/src/widgets/error_page.dart';
import 'package:flutter/material.dart';

typedef WorkItemsArgs = ({Project? project, SavedShortcut? shortcut});
typedef WorkItemDetailArgs = ({String project, int id});
typedef CreateOrEditWorkItemArgs = ({String? project, int? id, String? area, String? iteration});
typedef PullRequestArgs = ({Project? project, SavedShortcut? shortcut});
typedef PullRequestDetailArgs = ({String project, String repository, int id});
typedef CommitsArgs = ({Project? project, GraphUser? author});
typedef CommitsArgs = ({Project? project, GraphUser? author, SavedShortcut? shortcut});
typedef CommitDetailArgs = ({String project, String repository, String commitId});
typedef FileDiffArgs = ({Commit commit, String filePath, bool isAdded, bool isDeleted, int? pullRequestId});
typedef PipelineLogsArgs = ({String project, int pipelineId, String taskId, String parentTaskId, int logId});
typedef PipelinesArgs = ({Project? project, int? definition});
typedef PipelinesArgs = ({Project? project, int? definition, SavedShortcut? shortcut});

class AppRouter {
AppRouter._();
Expand Down Expand Up @@ -143,8 +146,8 @@ class AppRouter {

static PipelineLogsArgs getPipelineLogsArgs(BuildContext context) => _getArgs(context);

static Future<void> goToCommits({Project? project, GraphUser? author}) =>
_goTo(_commits, args: (project: project, author: author));
static Future<void> goToCommits({Project? project, GraphUser? author, SavedShortcut? shortcut}) =>
_goTo(_commits, args: (project: project, author: author, shortcut: shortcut));

static CommitsArgs? getCommitsArgs(BuildContext context) => _getArgs(context);

Expand All @@ -165,9 +168,9 @@ class AppRouter {

static String getProjectDetailArgs(BuildContext context) => _getArgs(context);

static Future<void> goToWorkItems({Project? project}) => _goTo(_workItems, args: project);
static Future<void> goToWorkItems({WorkItemsArgs? args}) => _goTo(_workItems, args: args);

static Project? getWorkItemsArgs(BuildContext context) => _getArgs(context);
static WorkItemsArgs? getWorkItemsArgs(BuildContext context) => _getArgs(context);

static Future<void> goToWorkItemDetail({required String project, required int id}) =>
_goTo<WorkItemDetailArgs>(_workItemDetail, args: (project: project, id: id));
Expand All @@ -179,9 +182,9 @@ class AppRouter {

static CreateOrEditWorkItemArgs getCreateOrEditWorkItemArgs(BuildContext context) => _getArgs(context);

static Future<void> goToPullRequests({Project? project}) => _goTo(_pullRequests, args: project);
static Future<void> goToPullRequests({PullRequestArgs? args}) => _goTo(_pullRequests, args: args);

static Project? getPullRequestsArgs(BuildContext context) => _getArgs(context);
static PullRequestArgs? getPullRequestsArgs(BuildContext context) => _getArgs(context);

static Future<void> goToPullRequestDetail({required String project, required String repository, required int id}) =>
_goTo<PullRequestDetailArgs>(_pullRequestDetail, args: (project: project, repository: repository, id: id));
Expand Down
1 change: 1 addition & 0 deletions lib/src/screens/commits/base_commits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:azure_devops/src/models/user.dart';
import 'package:azure_devops/src/router/router.dart';
import 'package:azure_devops/src/services/azure_api_service.dart';
import 'package:azure_devops/src/services/filters_service.dart';
import 'package:azure_devops/src/services/overlay_service.dart';
import 'package:azure_devops/src/services/storage_service.dart';
import 'package:azure_devops/src/theme/theme.dart';
import 'package:azure_devops/src/widgets/app_page.dart';
Expand Down
34 changes: 31 additions & 3 deletions lib/src/screens/commits/controller_commits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,36 @@ class _CommitsController with FilterMixin {
organization: apiService.organization,
);

/// Read/write filters from local storage only if user is not coming from project page
bool get shouldPersistFilters => args?.project == null;
/// Read/write filters from local storage only if user is not coming from project page or from saved filter
bool get shouldPersistFilters => args?.project == null && !hasShortcut;

bool get hasShortcut => args?.shortcut != null;

void dispose() {
instance = null;
}

Future<void> init() async {
if (shouldPersistFilters) _fillSavedFilters();
if (shouldPersistFilters) {
_fillSavedFilters();
} else if (hasShortcut) {
_fillShortcutFilters();
}

await _getData();
}

void _fillSavedFilters() {
final savedFilters = filtersService.getCommitsSavedFilters();
_fillFilters(savedFilters);
}

void _fillShortcutFilters() {
final savedFilters = filtersService.getCommitsShortcut(args!.shortcut!.label);
_fillFilters(savedFilters);
}

void _fillFilters(CommitsFilters savedFilters) {
if (savedFilters.projects.isNotEmpty) {
projectsFilter = getProjects(storageService).where((p) => savedFilters.projects.contains(p.name)).toSet();
}
Expand Down Expand Up @@ -133,4 +147,18 @@ class _CommitsController with FilterMixin {

init();
}

Future<void> saveFilters() async {
final shortcutLabel = await OverlayService.formBottomsheet(title: 'Choose a name', label: 'Name');
if (shortcutLabel == null) return;

final res = filtersService.saveCommitsShortcut(shortcutLabel, {
if (!isDefaultUsersFilter) CommitsFilters.authorsKey: usersFilter.map((u) => u.mailAddress!).toSet(),
if (!isDefaultProjectsFilter) CommitsFilters.projectsKey: projectsFilter.map((p) => p.name!).toSet(),
});

if (!res.result) {
OverlayService.snackbar(res.message, isError: true);
}
}
}
1 change: 1 addition & 0 deletions lib/src/screens/commits/screen_commits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class _CommitsScreen extends StatelessWidget {
onEmpty: 'No commits found',
header: () => FiltersRow(
resetFilters: ctrl.resetFilters,
saveFilters: ctrl.saveFilters,
filters: [
FilterMenu<Project>.multiple(
title: 'Projects',
Expand Down
1 change: 1 addition & 0 deletions lib/src/screens/home/base_home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:azure_devops/src/mixins/logger_mixin.dart';
import 'package:azure_devops/src/models/project.dart';
import 'package:azure_devops/src/router/router.dart';
import 'package:azure_devops/src/services/azure_api_service.dart';
import 'package:azure_devops/src/services/filters_service.dart';
import 'package:azure_devops/src/services/storage_service.dart';
import 'package:azure_devops/src/theme/dev_ops_icons_icons.dart';
import 'package:azure_devops/src/theme/theme.dart';
Expand Down
60 changes: 60 additions & 0 deletions lib/src/screens/home/components_home.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
part of home;

class _ShortcutRow extends StatelessWidget {
const _ShortcutRow({required this.shortcut, required this.onTap});

final SavedShortcut shortcut;
final void Function(SavedShortcut p) onTap;

@override
Widget build(BuildContext context) {
return NavigationButton(
margin: const EdgeInsets.only(top: 8),
inkwellKey: ValueKey(shortcut.label),
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
onTap: () => onTap(shortcut),
child: Row(
children: [
DecoratedBox(
decoration: BoxDecoration(
color: context.colorScheme.primary,
shape: BoxShape.circle,
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
switch (shortcut.area) {
FilterAreas.commits => DevOpsIcons.commit,
FilterAreas.pipelines => DevOpsIcons.pipeline,
FilterAreas.workItems => DevOpsIcons.task,
FilterAreas.pullRequests => DevOpsIcons.pullrequest,
_ => DevOpsIcons.task,
},
color: context.colorScheme.onPrimary,
size: 14,
),
),
),
const SizedBox(
width: 12,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
shortcut.label,
style: context.textTheme.bodyMedium,
),
Text(
shortcut.filters.map((f) => '${f.attribute}: ${f.filters.join(', ')}').join('\n'),
style: context.textTheme.labelSmall,
),
],
),
),
Icon(Icons.arrow_forward_ios),
],
),
);
}
}

class _ProjectCard extends StatelessWidget {
const _ProjectCard({
required this.parameters,
Expand Down
17 changes: 17 additions & 0 deletions lib/src/screens/home/controller_home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class _HomeController with AppLogger {
int _projectsCount = 0;
bool get hasManyProjects => _projectsCount > 10;

List<SavedShortcut> shortcuts = [];

void dispose() {
instance = null;
}
Expand All @@ -43,6 +45,8 @@ class _HomeController with AppLogger {

_hideSearchField();

shortcuts = storageService.getSavedShortcuts();

projects.value = ApiResponse.ok(sortedProjects);

storageService.setChosenProjects(existentProjects);
Expand Down Expand Up @@ -78,6 +82,19 @@ class _HomeController with AppLogger {
AppRouter.goToProjectDetail(p.name!);
}

void goToListPage(SavedShortcut shortcut) {
switch (shortcut.area) {
case FilterAreas.commits:
AppRouter.goToCommits(shortcut: shortcut);
case FilterAreas.pipelines:
AppRouter.goToPipelines(args: (definition: null, project: null, shortcut: shortcut));
case FilterAreas.workItems:
AppRouter.goToWorkItems(args: (project: null, shortcut: shortcut));
case FilterAreas.pullRequests:
AppRouter.goToPullRequests(args: (project: null, shortcut: shortcut));
}
}

void _configureSentryAndFirebase() {
final userId = apiService.user?.id ?? 'uknown-id';
final email = apiService.user?.emailAddress ?? 'unknown-user';
Expand Down
12 changes: 12 additions & 0 deletions lib/src/screens/home/screen_home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ class _HomeScreen extends StatelessWidget {
),
],
),
if (ctrl.shortcuts.isNotEmpty) ...[
SectionHeader.withIcon(
text: 'Saved filters',
icon: DevOpsIcons.list,
),
...ctrl.shortcuts.map(
(s) => _ShortcutRow(
shortcut: s,
onTap: ctrl.goToListPage,
),
),
],
if (ctrl.hasManyProjects)
_ProjectsHeaderWithSearchField(ctrl: ctrl)
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ class _PipelineDetailController with ShareMixin {
}

void goToPreviousRuns() {
AppRouter.goToPipelines(args: (definition: pipeline.definition!.id!, project: pipeline.project!));
AppRouter.goToPipelines(args: (definition: pipeline.definition!.id!, project: pipeline.project!, shortcut: null));
}
}

Expand Down
1 change: 1 addition & 0 deletions lib/src/screens/pipelines/base_pipelines.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:azure_devops/src/models/user.dart';
import 'package:azure_devops/src/router/router.dart';
import 'package:azure_devops/src/services/azure_api_service.dart';
import 'package:azure_devops/src/services/filters_service.dart';
import 'package:azure_devops/src/services/overlay_service.dart';
import 'package:azure_devops/src/services/storage_service.dart';
import 'package:azure_devops/src/theme/theme.dart';
import 'package:azure_devops/src/widgets/app_page.dart';
Expand Down
39 changes: 35 additions & 4 deletions lib/src/screens/pipelines/controller_pipelines.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ class _PipelinesController with FilterMixin {
int get queuedPipelines => pipelines.value?.data?.where((b) => b.status == PipelineStatus.notStarted).length ?? 0;
int get cancellingPipelines => pipelines.value?.data?.where((b) => b.status == PipelineStatus.cancelling).length ?? 0;

/// Read/write filters from local storage only if user is not coming from project page
bool get shouldPersistFilters => args?.project == null;

Set<String> pipelineNamesFilter = {};
PipelineResult resultFilter = PipelineResult.all;
PipelineStatus statusFilter = PipelineStatus.all;
Expand All @@ -57,6 +54,11 @@ class _PipelinesController with FilterMixin {

bool get showPipelineNamesFilter => getPipelineNames().isNotEmpty;

/// Read/write filters from local storage only if user is not coming from project page
bool get shouldPersistFilters => args?.project == null && !hasShortcut;

bool get hasShortcut => args?.shortcut != null;

void dispose() {
_stopTimer();

Expand All @@ -69,7 +71,11 @@ class _PipelinesController with FilterMixin {
}

Future<void> init() async {
if (shouldPersistFilters) _fillSavedFilters();
if (shouldPersistFilters) {
_fillSavedFilters();
} else if (hasShortcut) {
_fillShortcutFilters();
}

await _getData();

Expand All @@ -91,7 +97,15 @@ class _PipelinesController with FilterMixin {

void _fillSavedFilters() {
final savedFilters = filtersService.getPipelinesSavedFilters();
_fillFilters(savedFilters);
}

void _fillShortcutFilters() {
final savedFilters = filtersService.getPipelinesShortcut(args!.shortcut!.label);
_fillFilters(savedFilters);
}

void _fillFilters(PipelinesFilters savedFilters) {
if (savedFilters.projects.isNotEmpty) {
projectsFilter = getProjects(storageService).where((p) => savedFilters.projects.contains(p.name)).toSet();
}
Expand Down Expand Up @@ -244,4 +258,21 @@ class _PipelinesController with FilterMixin {
.toSet()
.sortedBy((s) => s.toLowerCase());
}

Future<void> saveFilters() async {
final shortcutLabel = await OverlayService.formBottomsheet(title: 'Choose a name', label: 'Name');
if (shortcutLabel == null) return;

final res = filtersService.savePipelinesShortcut(shortcutLabel, {
if (!isDefaultProjectsFilter) PipelinesFilters.projectsKey: projectsFilter.map((p) => p.name!).toSet(),
if (resultFilter != PipelineResult.all) PipelinesFilters.resultKey: {resultFilter.stringValue},
if (statusFilter != PipelineStatus.all) PipelinesFilters.statusKey: {statusFilter.stringValue},
if (!isDefaultUsersFilter) PipelinesFilters.triggeredByKey: usersFilter.map((u) => u.mailAddress!).toSet(),
if (pipelineNamesFilter.isNotEmpty) PipelinesFilters.pipelinesKey: pipelineNamesFilter,
});

if (!res.result) {
OverlayService.snackbar(res.message, isError: true);
}
}
}
1 change: 1 addition & 0 deletions lib/src/screens/pipelines/screen_pipelines.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class _PipelinesScreen extends StatelessWidget {
onEmpty: 'No pipelines found',
header: () => FiltersRow(
resetFilters: ctrl.resetFilters,
saveFilters: ctrl.saveFilters,
filters: [
if (ctrl.args?.definition == null)
FilterMenu<Project>.multiple(
Expand Down
6 changes: 3 additions & 3 deletions lib/src/screens/project_detail/controller_project_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ class _ProjectDetailController {
}

void goToPipelines() {
AppRouter.goToPipelines(args: (project: project.value?.data?.project, definition: null));
AppRouter.goToPipelines(args: (project: project.value?.data?.project, definition: null, shortcut: null));
}

void goToWorkItems() {
AppRouter.goToWorkItems(project: project.value?.data?.project);
AppRouter.goToWorkItems(args: (project: project.value?.data?.project, shortcut: null));
}

void goToPullRequests() {
AppRouter.goToPullRequests(project: project.value?.data?.project);
AppRouter.goToPullRequests(args: (project: project.value?.data?.project, shortcut: null));
}
}
Loading

0 comments on commit c901b60

Please sign in to comment.