diff --git a/client/src/app/core/services/filter-list.service.ts b/client/src/app/core/services/filter-list.service.ts index fda6e7510..fc3be6317 100644 --- a/client/src/app/core/services/filter-list.service.ts +++ b/client/src/app/core/services/filter-list.service.ts @@ -22,11 +22,15 @@ export interface OsFilter { } /** - * Describes a list of available options for a drop down menu of a filter + * Describes a list of available options for a drop down menu of a filter. + * A filter condition of null will be interpreted as a negative filter + * ('None of the other filter options'). + * Filter condition numbers/number arrays will be checked against numerical + * values and as id(s) for objects. */ export interface OsFilterOption { label: string; - condition: string | boolean | number; + condition: string | boolean | number | number[]; isActive?: boolean; } @@ -55,6 +59,47 @@ export abstract class FilterListService f.property === filterName); if (filter) { @@ -213,59 +264,101 @@ export abstract class FilterListService typeof option !== 'string' && option.isActive && option.condition === null + ); + let passesNullFilter = true; for (const option of filter.options) { + // ignored options if (typeof option === 'string') { continue; - } - if (option.isActive) { - if (option.condition === null) { - return this.checkIncludedNegative(item, filter); - } - if (item[filter.property] === undefined) { - return false; - } - if (item[filter.property] instanceof BaseModel) { - if (item[filter.property].id === option.condition) { - return true; - } - } else if (item[filter.property] === option.condition) { - return true; - } else if (item[filter.property].toString() === option.condition) { + } else if (nullFilter && option === nullFilter) { + continue; + // active option. The item is included if it passes this test + } else if (option.isActive) { + if (this.checkFilterIncluded(item, filter, option)) { return true; } + // if a null filter is set, the item needs to not pass all inactive filters + } else if ( + nullFilter && + (item[filter.property] !== null || item[filter.property] !== undefined) && + this.checkFilterIncluded(item, filter, option) + ) { + passesNullFilter = false; } } + if (nullFilter && passesNullFilter) { + return true; + } return false; } /** - * Returns true if none of the defined non-null filters apply, - * aka 'items that match no filter' - * @param item: A viewModel - * @param filter + * Checks an item against a single filter option. + * + * @param item A BaseModel to be checked + * @param filter The parent filter + * @param option The option to be checked + * @returns true if the filter condition matches the item */ - private checkIncludedNegative(item: V, filter: OsFilter): boolean { - if (item[filter.property] === undefined) { + private checkFilterIncluded(item: V, filter: OsFilter, option: OsFilterOption): boolean { + if (item[filter.property] === undefined || item[filter.property] === null) { + return false; + } else if (Array.isArray(item[filter.property])) { + const compareValueCondition = (value, condition): boolean => { + if (value === condition) { + return true; + } else if (value.hasOwnProperty('id') && value.id === condition) { + return true; + } + return false; + }; + for (const value of item[filter.property]) { + if (Array.isArray(option.condition)) { + for (const condition of option.condition) { + if (compareValueCondition(value, condition)) { + return true; + } + } + } else { + if (compareValueCondition(value, option.condition)) { + return true; + } + } + } + } else if (Array.isArray(option.condition)) { + if ( + option.condition.indexOf(item[filter.property]) > -1 || + option.condition.indexOf(item[filter.property].id) > -1 + ) { + return true; + } + } else if (typeof item[filter.property] === 'object' && item[filter.property].hasOwnProperty('id')) { + if (item[filter.property].id === option.condition) { + return true; + } + } else if (item[filter.property] === option.condition) { + return true; + } else if (item[filter.property].toString() === option.condition) { return true; } - for (const option of filter.options) { - if (typeof option === 'string' || option.condition === null) { - continue; - } - if (item[filter.property] === option.condition) { - return false; - } else if (item[filter.property].toString() === option.condition) { - return false; - } - } - return true; + return false; } + /** + * Retrieves a translatable label or filter property used for displaying the filter + * + * @param filter + * @returns a name, capitalized first character + */ public getFilterName(filter: OsFilter): string { if (filter.label) { return filter.label; @@ -275,20 +368,24 @@ export abstract class FilterListService { + if (typeof option === 'object' && option.isActive) { + this.removeFilterOption(filter.property, option); } - } - return filters; + }); } - public hasFilterOptions(): boolean { - return this.filterDefinitions && this.filterDefinitions.length ? true : false; + /** + * Removes all filters currently in use from this filterService + */ + public clearAllFilters(): void { + this.filterDefinitions.forEach(filter => { + this.clearFilter(filter); + }); } } diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.html b/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.html index c88910bb2..98290fdcd 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.html +++ b/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.html @@ -1,25 +1,54 @@ -
- - - - - +
+
+ {{ filterService.filteredCount }} of +  {{ filterService.totalCount }} +
+
+
+ Active filters:  +
+
+ +
+
+ +
+
+
+ + + + + +
diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.scss b/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.scss index e0eabd55b..efe2b5aa0 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.scss +++ b/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.scss @@ -8,3 +8,26 @@ span.right-with-margin { margin-right: 25px; } + +.flex-spaced { + display: flex; + justify-content: space-between; +} + +.filter-count { + text-align: right; + margin-right: 10px; + margin-left: 10px; + min-width: 50px; +} + +.current-filters { + display: flex; + justify-content: flex-start; + margin: 5px; + button { + padding: 3px; + font-size: 80%; + margin: 5px; + } +} diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.ts b/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.ts index fe3e37757..1081cd5c0 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.ts +++ b/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.ts @@ -40,7 +40,7 @@ export class OsSortFilterBarComponent { * be a FilterListService extendingFilterListService. */ @Input() - public filterService: any; // TODO a FilterListService extendingFilterListService + public filterService: any; // TODO a FilterListService extending FilterListService @Output() public searchFieldChange = new EventEmitter(); @@ -112,7 +112,7 @@ export class OsSortFilterBarComponent { * Checks if there is an active FilterService present */ public get hasFilters(): boolean { - if (this.filterService && this.filterService.hasFilterOptions()) { + if (this.filterService && this.filterService.hasFilterOptions) { return true; } return false; diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html index 33cd263c7..b41ec3364 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html @@ -13,78 +13,80 @@
- + + - - - - - - {{ isSelected(item) ? 'check_circle' : '' }} - - + + + + + + {{ isSelected(item) ? 'check_circle' : '' }} + + - - - Topic - - - check - {{ item.getListTitle() }} - - + + + Topic + + + check + {{ item.getListTitle() }} + + - - - Info - -
-
- visibility - {{ item.verboseType | translate }} + + + Info + +
+
+ visibility + {{ item.verboseType | translate }} +
+
+ access_time + {{ durationService.durationToString(item.duration) }} +
+
+ comment + {{ item.comment }} +
-
- access_time - {{ durationService.durationToString(item.duration) }} -
-
- comment - {{ item.comment }} -
-
- - + + - - - Speakers - - - - + + + Speakers + + + + - - - Menu - - - - + + + Menu + + + + - - - - + + + + +
diff --git a/client/src/app/site/agenda/services/agenda-filter-list.service.ts b/client/src/app/site/agenda/services/agenda-filter-list.service.ts index 557d6a14f..ce49e6a86 100644 --- a/client/src/app/site/agenda/services/agenda-filter-list.service.ts +++ b/client/src/app/site/agenda/services/agenda-filter-list.service.ts @@ -33,6 +33,7 @@ export class AgendaFilterListService extends FilterListService { options: [{ label: 'Open', condition: false }, { label: 'Closed', condition: true }] } ]; + this.updateFilterDefinitions(this.filterOptions); } private createVisibilityFilterOptions(): OsFilterOption[] { diff --git a/client/src/app/site/assignments/services/assignment-sort-list.service.ts b/client/src/app/site/assignments/services/assignment-sort-list.service.ts index f98deec3a..46b5c2bbb 100644 --- a/client/src/app/site/assignments/services/assignment-sort-list.service.ts +++ b/client/src/app/site/assignments/services/assignment-sort-list.service.ts @@ -10,8 +10,7 @@ export class AssignmentSortListService extends SortListService { sortProperty: 'assignment', sortAscending: true, options: [ - { property: 'agendaItem', label: 'agenda Item' }, - { property: 'assignment' }, + { property: 'assignment', label: 'Name' }, { property: 'phase' }, { property: 'candidateAmount', label: 'Number of candidates' } ] diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index 2c894dd52..e49286029 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -134,6 +134,15 @@ export class ViewMotion extends BaseViewModel { return this._state; } + /** + * Checks if the current state of thw workflow is final + * + * @returns true if it is final + */ + public get isFinalState(): boolean { + return this._state.isFinalState; + } + public get state_id(): number { return this.motion && this.motion.state_id ? this.motion.state_id : null; } @@ -209,6 +218,18 @@ export class ViewMotion extends BaseViewModel { return this._attachments ? this._attachments : null; } + /** + * Gets the comments' section ids of a motion. Used in filter by motionComment + * + * @returns an array of ids, or an empty array + */ + public get commentSectionIds(): number[] { + if (!this.motion) { + return []; + } + return this.motion.comments.map(comment => comment.section_id); + } + public constructor( motion?: Motion, category?: Category, diff --git a/client/src/app/site/motions/services/motion-filter-list.service.ts b/client/src/app/site/motions/services/motion-filter-list.service.ts index 86cb24882..1e9997f22 100644 --- a/client/src/app/site/motions/services/motion-filter-list.service.ts +++ b/client/src/app/site/motions/services/motion-filter-list.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { FilterListService, OsFilter } from '../../../core/services/filter-list.service'; +import { FilterListService, OsFilter, OsFilterOption } from '../../../core/services/filter-list.service'; import { Motion } from '../../../shared/models/motions/motion'; import { ViewMotion } from '../models/view-motion'; import { CategoryRepositoryService } from './category-repository.service'; @@ -8,6 +8,7 @@ import { WorkflowRepositoryService } from './workflow-repository.service'; import { StorageService } from '../../../core/services/storage.service'; import { MotionRepositoryService } from './motion-repository.service'; import { MotionBlockRepositoryService } from './motion-block-repository.service'; +import { MotionCommentSectionRepositoryService } from './motion-comment-section-repository.service'; @Injectable({ providedIn: 'root' @@ -19,19 +20,22 @@ export class MotionFilterListService extends FilterListService { + // return notesRepo.hasPersonalNote('Motion', m.id) + // } + // }; + + /** + * Subscibes to changing MotionBlocks, and updates the filter accordingly + */ private subscribeMotionBlocks(): void { this.motionBlockRepo.getViewModelListObservable().subscribe(motionBlocks => { const motionBlockOptions = []; @@ -87,20 +116,25 @@ export class MotionFilterListService extends FilterListService { - const categoryOptions = []; + const categoryOptions: (OsFilterOption | string)[] = []; categories.forEach(cat => { categoryOptions.push({ condition: cat.id, @@ -108,36 +142,96 @@ export class MotionFilterListService extends FilterListService { - const workflowOptions = []; + const workflowOptions: (OsFilterOption | string)[] = []; + const finalStates: number[] = []; + const nonFinalStates: number[] = []; + const recommendationOptions: (OsFilterOption | string)[] = []; workflows.forEach(workflow => { workflowOptions.push(workflow.name); + recommendationOptions.push(workflow.name); workflow.states.forEach(state => { + if (state.isFinalState) { + finalStates.push(state.id); + } else { + nonFinalStates.push(state.id); + } workflowOptions.push({ - condition: state.name, + condition: state.id, label: state.name, isActive: false }); + if (state.recommendation_label) { + recommendationOptions.push({ + condition: state.id, + label: state.recommendation_label, + isActive: false + }); + } }); }); - workflowOptions.push('-'); - workflowOptions.push({ - condition: null, - label: 'no workflow set', - isActive: false - }); + if (workflowOptions.length) { + workflowOptions.push('-'); + workflowOptions.push({ + label: 'Done', + condition: finalStates + }); + workflowOptions.push({ + label: 'Undone', + condition: nonFinalStates + }); + } + if (recommendationOptions.length) { + recommendationOptions.push('-'); + recommendationOptions.push({ + label: 'No recommendation', + condition: null + }); + } this.flowFilterOptions.options = workflowOptions; + this.recommendationFilterOptions.options = recommendationOptions; this.updateFilterDefinitions(this.filterOptions); }); } + /** + * Subscibes to changing Comments, and updates the filter accordingly + */ private subscribeComments(): void { - // TODO + this.commentRepo.getViewModelListObservable().subscribe(comments => { + const commentOptions: (OsFilterOption | string)[] = []; + comments.forEach(comm => { + commentOptions.push({ + condition: comm.id, + label: comm.name, + isActive: false + }); + }); + if (comments.length) { + commentOptions.push('-'); + commentOptions.push({ + label: 'No comment', + condition: null + }); + } + this.motionCommentFilterOptions.options = commentOptions; + this.updateFilterDefinitions(this.filterOptions); + }); } } diff --git a/client/src/app/site/users/services/user-filter-list.service.ts b/client/src/app/site/users/services/user-filter-list.service.ts index 7958e357a..8ee788e8a 100644 --- a/client/src/app/site/users/services/user-filter-list.service.ts +++ b/client/src/app/site/users/services/user-filter-list.service.ts @@ -63,7 +63,7 @@ export class UserFilterListService extends FilterListService { public subscribeGroups(): void { this.groupRepo.getViewModelListObservable().subscribe(groups => { const groupOptions = []; - groupOptions.forEach(group => { + groups.forEach(group => { groupOptions.push({ condition: group.name, label: group.name,