diff --git a/webapp/src/css/inbox.less b/webapp/src/css/inbox.less index 758447e252..ab70be1581 100644 --- a/webapp/src/css/inbox.less +++ b/webapp/src/css/inbox.less @@ -473,7 +473,7 @@ mm-analytics-filters { background-color: @background-color; } -.reports .inbox-items, .contacts .inbox-items, .messages .inbox-items { +.reports .inbox-items, .contacts .inbox-items, .messages .inbox-items, .tasks .inbox-items { overflow-y: hidden; overflow-x: hidden; height: 100%; @@ -1115,6 +1115,49 @@ mm-search-bar { } } +mm-filter-slider-icon { + padding: 8px 0; + position: relative; + display: block; + + .mm-filter-slider-container { + display: flex; + justify-content: flex-start; + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.8); + + *, *:active, *:focus { + outline: none !important; + box-shadow: none; + } + + .open-filter { + background-color: transparent; + margin: 0 3px; + font-size: @font-large; + position: relative; + border-radius: 0 px; + + &:active { + box-shadow: none; + } + + .filter-counter { + color: @chip-text-color; + font-size: @font-extra-extra-small; + background: @chip-background-color; + border-radius: 50%; + padding: 1px 5px; + vertical-align: middle; + } + } + + &.disabled { + color: @inactive-color; + } + } +} + .more-options-menu-container { margin-right: 20px; diff --git a/webapp/src/ts/components/components.module.ts b/webapp/src/ts/components/components.module.ts index f983ff0dd0..c294e42881 100644 --- a/webapp/src/ts/components/components.module.ts +++ b/webapp/src/ts/components/components.module.ts @@ -32,6 +32,7 @@ import { ReportImageComponent } from '@mm-components/report-image/report-image.c import { NavigationComponent } from '@mm-components/navigation/navigation.component'; import { EnketoComponent } from '@mm-components/enketo/enketo.component'; import { SearchBarComponent } from '@mm-components/search-bar/search-bar.component'; +import { FilterSliderIconComponent } from '@mm-components/filter-slider-icon/filter-slider-icon.component'; import { MultiselectBarComponent } from '@mm-components/multiselect-bar/multiselect-bar.component'; import { AnalyticsTargetsProgressComponent @@ -61,6 +62,7 @@ import { ToolBarComponent } from '@mm-components/tool-bar/tool-bar.component'; FreetextFilterComponent, FastActionButtonComponent, SearchBarComponent, + FilterSliderIconComponent, MultiselectBarComponent, SortFilterComponent, SenderComponent, @@ -104,6 +106,7 @@ import { ToolBarComponent } from '@mm-components/tool-bar/tool-bar.component'; FastActionButtonComponent, StatusFilterComponent, SearchBarComponent, + FilterSliderIconComponent, MultiselectBarComponent, FreetextFilterComponent, SortFilterComponent, diff --git a/webapp/src/ts/components/filter-slider-icon/filter-slider-icon.component.html b/webapp/src/ts/components/filter-slider-icon/filter-slider-icon.component.html new file mode 100644 index 0000000000..669ccdd039 --- /dev/null +++ b/webapp/src/ts/components/filter-slider-icon/filter-slider-icon.component.html @@ -0,0 +1,11 @@ +
+ +
\ No newline at end of file diff --git a/webapp/src/ts/components/filter-slider-icon/filter-slider-icon.component.ts b/webapp/src/ts/components/filter-slider-icon/filter-slider-icon.component.ts new file mode 100644 index 0000000000..55daa19ac1 --- /dev/null +++ b/webapp/src/ts/components/filter-slider-icon/filter-slider-icon.component.ts @@ -0,0 +1,84 @@ +import { + Component, + EventEmitter, + Input, + OnDestroy, + AfterContentInit, + AfterViewInit, + Output, +} from '@angular/core'; +import { Store } from '@ngrx/store'; +import { combineLatest, Subscription } from 'rxjs'; + +import { Selectors } from '@mm-selectors/index'; +import { ResponsiveService } from '@mm-services/responsive.service'; + +@Component({ + selector: 'mm-filter-slider-icon', + templateUrl: './filter-slider-icon.component.html' +}) +export class FilterSliderIconComponent implements AfterContentInit, AfterViewInit, OnDestroy { + @Input() disabled; + @Input() showFilter; + @Input() lastVisitedDateExtras; + @Output() toggleFilter: EventEmitter = new EventEmitter(); + + private filters; + subscription: Subscription = new Subscription(); + activeFilters: number = 0; + openSearch = false; + + constructor( + private store: Store, + private responsiveService: ResponsiveService, + ) { } + + ngAfterContentInit() { + this.subscribeToStore(); + } + + ngAfterViewInit() { + } + + private subscribeToStore() { + const subscription = combineLatest( + this.store.select(Selectors.getSidebarFilter), + this.store.select(Selectors.getFilters), + ).subscribe(([sidebarFilter, filters]) => { + this.activeFilters = sidebarFilter?.filterCount?.total || 0; + this.filters = filters; + + if (!this.openSearch && this.filters?.search) { + this.toggleMobileSearch(); + } + }); + this.subscription.add(subscription); + } + + clear() { + if (this.disabled) { + return; + } + this.toggleMobileSearch(false); + } + + toggleMobileSearch(forcedValue?) { + if (forcedValue === undefined && (this.disabled || !this.responsiveService.isMobile())) { + return; + } + + this.openSearch = forcedValue !== undefined ? forcedValue : !this.openSearch; + } + + showSearchIcon() { + return !this.openSearch && !this.filters?.search; + } + + showClearIcon() { + return this.openSearch || !!this.filters?.search; + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} \ No newline at end of file diff --git a/webapp/src/ts/modules/modules.module.ts b/webapp/src/ts/modules/modules.module.ts index 38705b91c5..524e46a3db 100644 --- a/webapp/src/ts/modules/modules.module.ts +++ b/webapp/src/ts/modules/modules.module.ts @@ -42,6 +42,7 @@ import { AnalyticsTargetAggregatesDetailComponent } from '@mm-modules/analytics/analytics-target-aggregates-detail.component'; import { TasksComponent } from '@mm-modules/tasks/tasks.component'; +import { TasksSidebarFilterComponent } from '@mm-modules/tasks/tasks-sidebar-filter.component'; import { TasksContentComponent } from '@mm-modules/tasks/tasks-content.component'; import { TasksGroupComponent } from '@mm-modules/tasks/tasks-group.component'; import { TestingComponent } from '@mm-modules/testing/testing.component'; @@ -75,6 +76,7 @@ import { DirectivesModule } from '@mm-directives/directives.module'; AnalyticsTargetAggregatesDetailComponent, AnalyticsTargetAggregatesSidebarFilterComponent, TasksComponent, + TasksSidebarFilterComponent, TasksContentComponent, TasksGroupComponent, TestingComponent, diff --git a/webapp/src/ts/modules/tasks/tasks-content.component.html b/webapp/src/ts/modules/tasks/tasks-content.component.html index 971bc950f8..21ae6caf00 100644 --- a/webapp/src/ts/modules/tasks/tasks-content.component.html +++ b/webapp/src/ts/modules/tasks/tasks-content.component.html @@ -1,55 +1,52 @@ -
- -
-
-
-
+
+
+
+
-
-
{{'No task selected' | translate}}
-
+
+
{{'No task selected' | translate}}
+
-
-
{{ errorTranslationKey | translate}}
-
+
+
{{ errorTranslationKey | translate}}
+
-
-
-
-
    -
  • -

    {{selectedTask.title | translateFrom:selectedTask}}

    -
  • -
  • - -

    -
  • -
  • - -

    - - - - - {{selectedTask.priorityLabel | translateFrom:selectedTask}} -

    -
  • -
  • - -

    {{field.value | translateFrom:selectedTask}}

    -
  • -
  • - {{action.label | translateFrom:selectedTask}} -
  • -
-
+
+
+
+
    +
  • +

    {{selectedTask.title | translateFrom:selectedTask}}

    +
  • +
  • + +

    +
  • +
  • + +

    + + + + + {{selectedTask.priorityLabel | translateFrom:selectedTask}} +

    +
  • +
  • + +

    {{field.value | translateFrom:selectedTask}}

    +
  • +
  • + {{action.label | translateFrom:selectedTask}} +
  • +
+
-
-
- -
+
+
+
-
+
\ No newline at end of file diff --git a/webapp/src/ts/modules/tasks/tasks-sidebar-filter.component.html b/webapp/src/ts/modules/tasks/tasks-sidebar-filter.component.html new file mode 100644 index 0000000000..df78ce3bbe --- /dev/null +++ b/webapp/src/ts/modules/tasks/tasks-sidebar-filter.component.html @@ -0,0 +1,49 @@ + + + + + diff --git a/webapp/src/ts/modules/tasks/tasks-sidebar-filter.component.ts b/webapp/src/ts/modules/tasks/tasks-sidebar-filter.component.ts new file mode 100644 index 0000000000..3f2434d654 --- /dev/null +++ b/webapp/src/ts/modules/tasks/tasks-sidebar-filter.component.ts @@ -0,0 +1,95 @@ +import { AfterViewInit, Component, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { GlobalActions } from '@mm-actions/global'; +import { FacilityFilterComponent } from '@mm-components/filters/facility-filter/facility-filter.component'; +import { TelemetryService } from '@mm-services/telemetry.service'; + +type FilterComponent = FacilityFilterComponent; + +@Component({ + selector: 'mm-tasks-sidebar-filter', + templateUrl: './tasks-sidebar-filter.component.html' +}) +export class TasksSidebarFilterComponent implements AfterViewInit, OnDestroy { + @Output() search: EventEmitter = new EventEmitter(); + @Input() disabled; + + @ViewChild(FacilityFilterComponent) + facilityFilter: FacilityFilterComponent; + + private globalActions; + private filters: FilterComponent[] = []; + isResettingFilters = false; + isOpen = false; + filterCount:any = { }; + dateFilterError = ''; + + constructor( + private store: Store, + private telemetryService: TelemetryService, + ) { + this.globalActions = new GlobalActions(store); + } + + ngAfterViewInit() { + this.filters = [ + this.facilityFilter, + ]; + } + + applyFilters() { + if (this.isResettingFilters || this.disabled) { + return; + } + this.search.emit(); + this.countSelected(); + } + + clearFilters(fieldIds?) { + if (this.disabled) { + return; + } + const filters = fieldIds ? this.filters.filter(filter => fieldIds.includes(filter.fieldId)) : this.filters; + filters.forEach(filter => filter.clear()); + } + + countSelected() { + this.filterCount.total = 0; + this.filters.forEach(filter => { + const count = filter.countSelected() || 0; + this.filterCount.total += count; + this.filterCount[filter.fieldId] = count; + }); + this.globalActions.setSidebarFilter({ filterCount: { ...this.filterCount }}); + } + + resetFilters() { + if (this.disabled) { + return; + } + this.isResettingFilters = true; + this.globalActions.clearFilters('search'); + this.clearFilters(); + this.isResettingFilters = false; + this.applyFilters(); + } + + toggleSidebarFilter() { + this.isOpen = !this.isOpen; + this.globalActions.setSidebarFilter({ isOpen: this.isOpen }); + } + + showDateFilterError(error) { + this.dateFilterError = error || ''; + } + + setDefaultFacilityFilter(filters) { + this.facilityFilter?.setDefault(filters?.facility); + } + + ngOnDestroy() { + this.globalActions.clearSidebarFilter(); + this.globalActions.clearFilters(); + } +} \ No newline at end of file diff --git a/webapp/src/ts/modules/tasks/tasks.component.html b/webapp/src/ts/modules/tasks/tasks.component.html index bad7260cce..d7a4efbdb5 100644 --- a/webapp/src/ts/modules/tasks/tasks.component.html +++ b/webapp/src/ts/modules/tasks/tasks.component.html @@ -1,9 +1,24 @@ +
+
+ + + +
+
+
-
+
+
- +
+
+ +
+ + + +
diff --git a/webapp/src/ts/modules/tasks/tasks.component.ts b/webapp/src/ts/modules/tasks/tasks.component.ts index a68c342b8d..0e9524a849 100644 --- a/webapp/src/ts/modules/tasks/tasks.component.ts +++ b/webapp/src/ts/modules/tasks/tasks.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, AfterViewInit, ViewChild } from '@angular/core'; import { Store } from '@ngrx/store'; import { combineLatest, Subscription } from 'rxjs'; import { debounce as _debounce } from 'lodash-es'; @@ -13,11 +13,14 @@ import { GlobalActions } from '@mm-actions/global'; import { LineageModelGeneratorService } from '@mm-services/lineage-model-generator.service'; import { PerformanceService } from '@mm-services/performance.service'; import { ExtractLineageService } from '@mm-services/extract-lineage.service'; +import { TasksSidebarFilterComponent } from '@mm-modules/tasks/tasks-sidebar-filter.component'; @Component({ templateUrl: './tasks.component.html', }) -export class TasksComponent implements OnInit, OnDestroy { +export class TasksComponent implements OnInit, AfterViewInit, OnDestroy { + @ViewChild(TasksSidebarFilterComponent) tasksSidebarFilter: TasksSidebarFilterComponent; + constructor( private store: Store, private changesService: ChangesService, @@ -39,11 +42,17 @@ export class TasksComponent implements OnInit, OnDestroy { tasksList; selectedTask; + selectedTasks; errorStack; hasTasks; loading; tasksDisabled; userLineageLevel; + filters: any = {}; + selectMode = false; + selectModeAvailable = false; + useSidebarFilter = true; + isSidebarFilterOpen = false; private tasksLoaded; private debouncedReload; @@ -57,11 +66,15 @@ export class TasksComponent implements OnInit, OnDestroy { const taskList$ = combineLatest([ this.store.select(Selectors.getTasksList), this.store.select(Selectors.getSelectedTask), + this.store.select(Selectors.getFilters), ]).subscribe(([ tasksList = [], selectedTask, + filters, ]) => { this.selectedTask = selectedTask; + this.filters = filters; + // Make new reference because the one from store is read-only. Fixes: ExpressionChangedAfterItHasBeenCheckedError this.tasksList = tasksList.map(task => ({ ...task, selected: task._id === this.selectedTask?._id })); }); @@ -106,6 +119,10 @@ export class TasksComponent implements OnInit, OnDestroy { this.refreshTasks(); } + async ngAfterViewInit() { + this.subscribeSidebarFilter(); + } + ngOnDestroy() { this.subscription.unsubscribe(); this.tasksActions.setTasksList([]); @@ -119,6 +136,17 @@ export class TasksComponent implements OnInit, OnDestroy { window.location.reload(); } + private subscribeSidebarFilter() { + if (!this.useSidebarFilter) { + return; + } + + const subscription = this.store + .select(Selectors.getSidebarFilter) + .subscribe(({ isOpen }) => this.isSidebarFilterOpen = !!isOpen); + this.subscription.add(subscription); + } + private hydrateEmissions(taskDocs) { return taskDocs.map(taskDoc => { const emission = { ...taskDoc.emission }; @@ -141,7 +169,7 @@ export class TasksComponent implements OnInit, OnDestroy { const taskDocs = isEnabled ? await this.rulesEngineService.fetchTaskDocsForAllContacts() : []; this.hasTasks = taskDocs.length > 0; - const hydratedTasks = await this.hydrateEmissions(taskDocs) || []; + let hydratedTasks = await this.hydrateEmissions(taskDocs) || []; const subjects = await this.getLineagesFromTaskDocs(hydratedTasks); if (subjects?.size) { const userLineageLevel = await this.userLineageLevel; @@ -150,6 +178,19 @@ export class TasksComponent implements OnInit, OnDestroy { }); } + if (subjects?.size) { + const userLineageLevel = await this.currentLevel; + hydratedTasks.forEach(task => { + task.lineageId = this.getTaskLineageById(subjects, task, userLineageLevel); + }); + } + + if(this.filters?.facilities) { + hydratedTasks = hydratedTasks.filter(task => + task.lineageId.some(value => this.filters.facilities.selected.includes(value)) + ); + } + this.tasksActions.setTasksList(hydratedTasks); } catch (exception) { @@ -185,6 +226,18 @@ export class TasksComponent implements OnInit, OnDestroy { return task?._id; } + toggleFilter() { + this.tasksSidebarFilter?.toggleSidebarFilter(); + } + + resetFilter() { + this.tasksSidebarFilter?.resetFilters(); + } + + search() { + this.refreshTasks(); + } + private getLineagesFromTaskDocs(taskDocs) { const ids = [ ...new Set(taskDocs.map(task => task.owner)) ]; return this.lineageModelGeneratorService @@ -198,4 +251,11 @@ export class TasksComponent implements OnInit, OnDestroy { ?.map(lineage => lineage?.name); return this.extractLineageService.removeUserFacility(lineage, userLineageLevel); } + + private getTaskLineageById(subjects, task, userLineageLevel) { + const lineage = subjects + .get(task.owner) + ?.map(lineage => lineage?._id); + return this.extractLineageService.removeUserFacility(lineage, userLineageLevel); + } }