Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement named filters #31

Merged
merged 11 commits into from
Jan 5, 2024
6 changes: 6 additions & 0 deletions lib/src/extensions/string_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ extension StringExt on String {

return this;
}

String get titleCase {
if (isEmpty) return this;

return '${substring(0, 1).toUpperCase()}${substring(1)}';
}
}
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
2 changes: 2 additions & 0 deletions lib/src/screens/commits/base_commits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ 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';
import 'package:azure_devops/src/widgets/commit_list_tile.dart';
import 'package:azure_devops/src/widgets/filter_menu.dart';
import 'package:azure_devops/src/widgets/shortcut_label.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

Expand Down
35 changes: 32 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 shortcut
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,19 @@ 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,
filters: CommitsFilters(
projects: projectsFilter.map((p) => p.name!).toSet(),
authors: usersFilter.map((u) => u.mailAddress!).toSet(),
),
);

OverlayService.snackbar(res.message, isError: !res.result);
}
}
59 changes: 33 additions & 26 deletions lib/src/screens/commits/screen_commits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,39 @@ class _CommitsScreen extends StatelessWidget {
showScrollbar: true,
onResetFilters: ctrl.resetFilters,
onEmpty: 'No commits found',
header: () => FiltersRow(
resetFilters: ctrl.resetFilters,
filters: [
FilterMenu<Project>.multiple(
title: 'Projects',
values: ctrl.getProjects(ctrl.storageService, withProjectAll: false),
currentFilters: ctrl.projectsFilter,
onSelectedMultiple: ctrl.filterByProjects,
formatLabel: (p) => p.name!,
isDefaultFilter: ctrl.isDefaultProjectsFilter,
widgetBuilder: (p) => ProjectFilterWidget(project: p),
onSearchChanged:
ctrl.hasManyProjects(ctrl.storageService) ? (s) => ctrl.searchProject(s, ctrl.storageService) : null,
),
FilterMenu<GraphUser>.multiple(
title: 'Authors',
values: ctrl.getSortedUsers(ctrl.apiService, withUserAll: false),
currentFilters: ctrl.usersFilter,
onSelectedMultiple: ctrl.filterByUsers,
formatLabel: (u) => ctrl.getFormattedUser(u, ctrl.apiService),
isDefaultFilter: ctrl.isDefaultUsersFilter,
widgetBuilder: (u) => UserFilterWidget(user: u),
onSearchChanged: ctrl.hasManyUsers(ctrl.apiService) ? (s) => ctrl.searchUser(s, ctrl.apiService) : null,
),
],
),
header: () => ctrl.hasShortcut
? ShortcutLabel(
label: ctrl.args!.shortcut!.label,
)
: FiltersRow(
resetFilters: ctrl.resetFilters,
saveFilters: ctrl.saveFilters,
filters: [
FilterMenu<Project>.multiple(
title: 'Projects',
values: ctrl.getProjects(ctrl.storageService, withProjectAll: false),
currentFilters: ctrl.projectsFilter,
onSelectedMultiple: ctrl.filterByProjects,
formatLabel: (p) => p.name!,
isDefaultFilter: ctrl.isDefaultProjectsFilter,
widgetBuilder: (p) => ProjectFilterWidget(project: p),
onSearchChanged: ctrl.hasManyProjects(ctrl.storageService)
? (s) => ctrl.searchProject(s, ctrl.storageService)
: null,
),
FilterMenu<GraphUser>.multiple(
title: 'Authors',
values: ctrl.getSortedUsers(ctrl.apiService, withUserAll: false),
currentFilters: ctrl.usersFilter,
onSelectedMultiple: ctrl.filterByUsers,
formatLabel: (u) => ctrl.getFormattedUser(u, ctrl.apiService),
isDefaultFilter: ctrl.isDefaultUsersFilter,
widgetBuilder: (u) => UserFilterWidget(user: u),
onSearchChanged:
ctrl.hasManyUsers(ctrl.apiService) ? (s) => ctrl.searchUser(s, ctrl.apiService) : null,
),
],
),
builder: (commits) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: commits!
Expand Down
5 changes: 5 additions & 0 deletions lib/src/screens/home/base_home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import 'dart:async';

import 'package:azure_devops/main.dart';
import 'package:azure_devops/src/extensions/context_extension.dart';
import 'package:azure_devops/src/extensions/string_extension.dart';
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/overlay_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';
import 'package:azure_devops/src/widgets/app_page.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:azure_devops/src/widgets/section_header.dart';
import 'package:azure_devops/src/widgets/work_card.dart';
Expand All @@ -21,6 +25,7 @@ import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:visibility_detector/visibility_detector.dart';

part 'components_home.dart';
part 'controller_home.dart';
Expand Down
91 changes: 91 additions & 0 deletions lib/src/screens/home/components_home.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,96 @@
part of home;

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

final SavedShortcut shortcut;
final void Function(SavedShortcut) onTap;
final void Function(SavedShortcut) onShowDetail;
final void Function(SavedShortcut) onRename;
final void Function(SavedShortcut) onDelete;

@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,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Text(
shortcut.area,
style: context.textTheme.labelSmall,
),
],
),
),
DevOpsPopupMenu(
tooltip: 'Shortcut ${shortcut.label} actions',
offset: const Offset(0, 20),
items: () => [
PopupItem(
onTap: () => onShowDetail(shortcut),
text: 'Show filters',
icon: DevOpsIcons.filter,
),
PopupItem(
onTap: () => onRename(shortcut),
text: 'Rename',
icon: DevOpsIcons.edit,
),
PopupItem(
onTap: () => onDelete(shortcut),
text: 'Delete',
icon: DevOpsIcons.trash,
),
],
),
],
),
);
}
}

class _ProjectCard extends StatelessWidget {
const _ProjectCard({
required this.parameters,
Expand Down
Loading