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 Presets for Filters #404

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
104 changes: 104 additions & 0 deletions src/app/core/models/preset.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Filter, FiltersService } from '../services/filters.service';
import { Repo } from './repo.model';

/**
* Represents a set of filters.
* Currently, the filters are saved as a URL-encoded string.
*/
export class Preset {
static VERSION = 1;
version = Preset.VERSION;
repo: Repo;
filter: Partial<Filter> | Filter;
label: string;
id: string; // current timestamp in ms as string

// If the preset is global, it will be available for all repos, and we will not save Milestone, Assignees and Labels.
isGlobal: boolean;

/** Creates a new Preset */
// constructor(repo: Repo, filter: Filter, label: string, id = Date.now().toString(), version = Preset.VERSION) {
// this.repo = repo;
// this.filter = filter;
// this.label = label;
// this.id = id;
// }
constructor({
repo,
filter,
label,
id = Date.now().toString(),
version = Preset.VERSION,
isGlobal = false
}: {
repo: Repo;
filter: Partial<Filter> | Filter;
label: string;
id?: string;
version?: number;
isGlobal: boolean;
}) {
this.repo = repo;

if (isGlobal) {
// filter.milestones = [];
// filter.assignees = [];
// filter.labels = [];
// filter.hiddenLabels = new Set();
// filter.deselectedLabels = new Set();
delete filter.milestones;
delete filter.assignees;
delete filter.labels;
delete filter.hiddenLabels;
delete filter.deselectedLabels;
this.filter = filter as Partial<Filter>;
} else {
this.filter = filter;
}

this.label = label;
this.id = id;
this.version = version;
this.isGlobal = isGlobal;
}

static fromObject(object: any): Preset {
const repo = Repo.fromObject(object.repo);
const isGlobal = object.isGlobal || false;
const filter = FiltersService.fromObject(object.filter, isGlobal);
const label = object.label;
const version = object.version || -1;

return new Preset({ repo, filter, label, id: object.id, version, isGlobal: object.isGlobal });
}

public toText(): string {
if (this.isGlobal) {
return this.summarizeGlobal();
} else {
return this.summarize();
}
}

/**
* Returns the filter as a summary string.
*
* TODO: https://github.com/CATcher-org/WATcher/issues/405
* This should be part of the filter model.
*/
private summarize() {
const filter = this.filter;
return `status:${filter.status} + type:${filter.type} + sort:${filter.sort.active}-${filter.sort.direction} + labels:${filter.labels} + milestones:${filter.milestones} + hiddenLabels:${filter.hiddenLabels} + deselectedLabels:${filter.deselectedLabels} + itemsPerPage:${filter.itemsPerPage} + assignees:${filter.assignees}`;
}

/**
* Returns the filter as a summary string.
*
* TODO: https://github.com/CATcher-org/WATcher/issues/405
* This should be part of the filter model.
*/
private summarizeGlobal() {
const filter = this.filter;
return `status:${filter.status} + type:${filter.type} + sort:${filter.sort.active}-${filter.sort.direction} + itemsPerPage:${filter.itemsPerPage}`;
}
}
9 changes: 6 additions & 3 deletions src/app/core/models/repo.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import { ErrorMessageService } from '../services/error-message.service';
* Repository url is owner/name.
*/
export class Repo {
owner: string;
name: string;

/** Creates a new Repo from owner and name strings. */
constructor(owner: string, name: string) {
this.owner = owner;
this.name = name;
}
owner: string;
name: string;

/** Creates a new Repo from one repository url. */
public static of(repoUrlInput: string) {
Expand Down Expand Up @@ -44,6 +43,10 @@ export class Repo {
return formattedInput.split('/').slice(-2).join('/');
}

public static fromObject(object: any): Repo {
return new Repo(object.owner, object.name);
}

/** String representation of a Repo. */
public toString(): string {
return this.owner + '/' + this.name;
Expand Down
173 changes: 162 additions & 11 deletions src/app/core/services/filters.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Injectable } from '@angular/core';
import { ComponentFactoryResolver, Injectable } from '@angular/core';
import { Sort } from '@angular/material/sort';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, pipe } from 'rxjs';
import { OrderOptions, SortOptions, StatusOptions, TypeOptions } from '../constants/filter-options.constants';
import { GithubUser } from '../models/github-user.model';
import { SimpleLabel } from '../models/label.model';
import { Milestone } from '../models/milestone.model';
import { Preset } from '../models/preset.model';
import { AssigneeService } from './assignee.service';
import { LoggingService } from './logging.service';
import { MilestoneService } from './milestone.service';
Expand All @@ -23,6 +24,10 @@ export type Filter = {
assignees: string[];
};

type QueryParams = {
[x: string]: any;
};

@Injectable({
providedIn: 'root'
})
Expand All @@ -31,6 +36,19 @@ export type Filter = {
* Filters are subscribed to and emitted from this service
*/
export class FiltersService {
constructor(
private logger: LoggingService,
private router: Router,
private route: ActivatedRoute,
private milestoneService: MilestoneService,
private assigneeService: AssigneeService
) {
this.filter$.subscribe((filter: Filter) => {
this.itemsPerPage = filter.itemsPerPage;
});

console.log(`Initialized Filters to `, { filters: this.filter$.value });
}
public static readonly PRESET_VIEW_QUERY_PARAM_KEY = 'presetview';
private itemsPerPage = 20;

Expand Down Expand Up @@ -86,16 +104,146 @@ export class FiltersService {
private previousMilestonesLength = 0;
private previousAssigneesLength = 0;

constructor(
private logger: LoggingService,
private router: Router,
private route: ActivatedRoute,
private milestoneService: MilestoneService,
private assigneeService: AssigneeService
) {
this.filter$.subscribe((filter: Filter) => {
this.itemsPerPage = filter.itemsPerPage;
});
/**
* Create a filter from a plain JSON object.
*
* TODO: https://github.com/CATcher-org/WATcher/issues/405
*
* @param object The object to create from e.g. from LocalStorage
* @returns
*/
static fromObject(object: any, isGlobal = false): Partial<Filter> {
console.log({ object });

if (isGlobal) {
const filter: Partial<Filter> = {
title: object.title,
status: object.status,
type: object.type,
sort: object.sort,
itemsPerPage: object.itemsPerPage
};

return filter;
} else {
const filter: Filter = {
title: object.title,
status: object.status,
type: object.type,
sort: object.sort,
labels: object.labels,
milestones: object.milestones,
hiddenLabels: new Set(Object.keys(object.hiddenLabels).length ? object.hiddenLabels : undefined),
deselectedLabels: new Set(Object.keys(object.deselectedLabels).length ? object.deselectedLabels : undefined),
itemsPerPage: object.itemsPerPage,
assignees: object.assignees
};

return filter;
}

// if (isGlobal) {
// filter.milestones = [];
// filter.assignees = [];
// filter.labels = [];
// filter.hiddenLabels = new Set();
// filter.deselectedLabels = new Set();
// }

// return filter;
}

/**
* Checks to see if two filters are equal.
* TODO: https://github.com/CATcher-org/WATcher/issues/405
* @param a The filter that is set in the app
* @param b The filter that comes from saving a preset
* @returns
*/
public static isPartOfPreset(a: Filter, preset: Preset): boolean {
// only compare if both objects have the key
// Compare simple scalar fields
const b = preset.filter;
if (a.title !== b.title) {
return false;
}
if (a.type !== b.type) {
return false;
}
if (a.itemsPerPage !== b.itemsPerPage) {
return false;
}
if (!FiltersService.haveSameElements(a.status, b.status)) {
return false;
}
// Compare Angular Material Sort (shallow comparison is enough)
if (!FiltersService.compareMatSort(a.sort, b.sort)) {
return false;
}

if (preset.isGlobal) {
return true;
}

// Compare arrays ignoring order
if (!FiltersService.haveSameElements(a.labels, b.labels)) {
return false;
}
if (!FiltersService.haveSameElements(a.milestones, b.milestones)) {
return false;
}
if (!FiltersService.haveSameElements(a.assignees, b.assignees)) {
return false;
}

// Compare sets
if (!FiltersService.areSetsEqual(a.hiddenLabels, b.hiddenLabels)) {
return false;
}
if (!FiltersService.areSetsEqual(a.deselectedLabels, b.deselectedLabels)) {
return false;
}

return true;
}

/**
* Returns true if two arrays contain exactly the same elements (ignoring order).
*/
private static haveSameElements(arr1: string[], arr2: string[]): boolean {
if (arr1.length !== arr2.length) {
return false;
}
const sorted1 = [...arr1].sort();
const sorted2 = [...arr2].sort();
return sorted1.every((val, idx) => val === sorted2[idx]);
}

/**
* Returns true if two sets contain exactly the same elements.
*
* TODO: https://github.com/CATcher-org/WATcher/issues/405
*/
private static areSetsEqual(set1: Set<string>, set2: Set<string>): boolean {
if (set1.size !== set2.size) {
return false;
}
for (const item of set1) {
if (!set2.has(item)) {
return false;
}
}
return true;
}

/**
* Compare two Angular Material Sort objects for equality.
*
* TODO: https://github.com/CATcher-org/WATcher/issues/405
*/
private static compareMatSort(s1: Sort, s2: Sort): boolean {
// Both 'active' and 'direction' are simple scalar fields
return s1.active === s2.active && s1.direction === s2.direction;
}

private pushFiltersToUrl(): void {
Expand Down Expand Up @@ -216,10 +364,13 @@ export class FiltersService {
}

updateFilters(newFilters: Partial<Filter>): void {
console.log({ newFilters }, 'Updating filters');
console.log({ oldFilters: this.filter$.value });
const nextDropdownFilter: Filter = {
...this.filter$.value,
...newFilters
};
console.log({ nextDropdownFilter });
this.filter$.next(nextDropdownFilter);
this.updatePresetViewFromFilters(newFilters);
this.pushFiltersToUrl();
Expand Down
Loading