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

Add dynamic counts to search filters #5471

Merged
merged 80 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 79 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
cc610d6
Remove color function
jorg-vr Apr 4, 2024
810fa47
Remove paramval function
jorg-vr Apr 4, 2024
84872dc
Pass through filter collection objects
jorg-vr Apr 4, 2024
0447296
Revert "Pass through filter collection objects"
jorg-vr Apr 4, 2024
b7a03dc
create flecible count by filters
jorg-vr Apr 5, 2024
bf8764c
Return named filter info
jorg-vr Apr 5, 2024
6f71249
Replace autoinclude by class method
jorg-vr Apr 5, 2024
ab642b6
Make filter options available in controller
jorg-vr Apr 5, 2024
fb772c4
Display counts
jorg-vr Apr 18, 2024
a51f6c2
Add support for multi filters
jorg-vr Apr 19, 2024
8beadd3
Fix bug
jorg-vr Apr 19, 2024
b7dfa43
fix non standerdize name options
jorg-vr Apr 19, 2024
95138fe
Create custom count functions
jorg-vr Apr 19, 2024
befc03a
Simplify js
jorg-vr Apr 19, 2024
f37d232
Autoupdate
jorg-vr Apr 19, 2024
4e7e410
Fix translations
jorg-vr Apr 19, 2024
8dde4f7
Add disabeled state to filters
jorg-vr Apr 19, 2024
b11b565
Convert activity read states
jorg-vr Apr 19, 2024
5293075
Rename by id filters
jorg-vr Apr 22, 2024
3459c41
Merge branch 'main' into chore/simplify-search
jorg-vr Apr 22, 2024
46acf6f
Fix filters for questions
jorg-vr Apr 23, 2024
f39264f
Fix course member search
jorg-vr Apr 23, 2024
60c073c
Fix course
jorg-vr Apr 23, 2024
2e3ebcc
Fix course
jorg-vr Apr 23, 2024
8792545
Fix course label filters
jorg-vr Apr 29, 2024
62a16f8
Fix course labels filters for evaluations
jorg-vr Apr 30, 2024
57110bb
Fix event types
jorg-vr Apr 30, 2024
457f928
COnvert feedbacks searchbar
jorg-vr Apr 30, 2024
8fa9e88
Fix repository searchbars
jorg-vr Apr 30, 2024
7030d61
Fix saved annotations
jorg-vr Apr 30, 2024
bebda0f
Fix series
jorg-vr Apr 30, 2024
3d2e327
Fix submissions
jorg-vr Apr 30, 2024
8d92875
fix users
jorg-vr Apr 30, 2024
f07a7d3
Simplify enums
jorg-vr Apr 30, 2024
2c80ae8
Simplify model naming
jorg-vr Apr 30, 2024
f9be4c9
remove unused code
jorg-vr May 2, 2024
98bdf22
Always include hasfilter
jorg-vr May 2, 2024
2d22ea1
Always include filterable
jorg-vr May 2, 2024
559b800
Fix javascript tests
jorg-vr May 2, 2024
0d95942
Merge branch 'chore/simplify-search' of github.com:dodona-edu/dodona …
jorg-vr May 2, 2024
d413a4a
Revert "Always include hasfilter"
jorg-vr May 2, 2024
16763ba
Fix rails tests
jorg-vr May 2, 2024
609dcec
Fix linting
jorg-vr May 2, 2024
f2215c4
Move color responsibility to frontend
jorg-vr May 2, 2024
7e581df
Simply xourse controller scoresheet
jorg-vr May 2, 2024
deb9809
Fix linting
jorg-vr May 2, 2024
a0cde25
Add count to searchfield suggestions
jorg-vr May 2, 2024
4a5c754
Remove unused scope
jorg-vr May 2, 2024
fae53bd
Remove unused scope
jorg-vr May 2, 2024
c40bbdc
Merge branch 'main' into chore/simplify-search
jorg-vr May 13, 2024
f7b5ec8
Avoid string repeat in type
jorg-vr May 13, 2024
e9e0887
Add documentation
jorg-vr May 13, 2024
e89c14a
Make numbers less prominent
jorg-vr May 13, 2024
9f054c6
Remove inline style
jorg-vr May 13, 2024
ff66570
Remove inline sql
jorg-vr May 14, 2024
698b7b9
Only do reselect on group by clauses
jorg-vr May 14, 2024
632fdde
Remove counts for submissions table
jorg-vr May 14, 2024
edd9fca
Fix course filter on all courses page
jorg-vr May 14, 2024
b934156
Add clear all active filters link
jorg-vr May 16, 2024
da4d108
Calculate question filter options after everything filter
jorg-vr May 16, 2024
726a1d6
Update app/controllers/feedbacks_controller.rb
jorg-vr May 22, 2024
174fc2a
Move filterable by course labels to its own concern
jorg-vr May 22, 2024
adf67d8
Only include filterable when needed
jorg-vr May 22, 2024
1a69157
Fix imports
jorg-vr May 22, 2024
77d6f3f
Merge branch 'main' into chore/simplify-search
jorg-vr May 22, 2024
f78d162
Hide 'define_singleton_method' behind method
jorg-vr May 22, 2024
0e89451
Remove delegation
jorg-vr May 22, 2024
8e2ce0b
Don't modify parent
jorg-vr May 22, 2024
b8171b2
Fix rubocop
jorg-vr May 22, 2024
14528ea
Merge branch 'main' into chore/simplify-search
jorg-vr May 28, 2024
9d872d8
No need for extra keys
jorg-vr May 28, 2024
25930a7
Merge branch 'main' into chore/simplify-search
jorg-vr Jun 25, 2024
21dd7dd
Merge branch 'main' into chore/simplify-search
jorg-vr Nov 13, 2024
4c0adbc
Fix linting
jorg-vr Nov 13, 2024
52025b1
Merge branch 'main' into chore/simplify-search
jorg-vr Nov 13, 2024
1f11ba3
Fix tests
jorg-vr Nov 13, 2024
a5c64f2
Use json_escape
jorg-vr Nov 13, 2024
e7a4ac7
Remove unused translations key
jorg-vr Nov 13, 2024
0c2bc88
Remove unused filter statement
jorg-vr Nov 13, 2024
2c4b3cf
Rename consistency
jorg-vr Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import { searchQueryState } from "state/SearchQuery";
import { DodonaElement } from "components/meta/dodona_element";
import { i18n } from "i18n/i18n";
import { FilterOptions } from "components/search/filter_element";
import { search } from "search";

Check warning on line 9 in app/assets/javascripts/components/saved_annotations/saved_annotation_list.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/components/saved_annotations/saved_annotation_list.ts#L9

Added line #L9 was not covered by tests

/**
* This component represents a list of saved annotations
Expand Down Expand Up @@ -43,7 +45,19 @@
return pagination;
}

lastFilters: FilterOptions[] = [];
get filters(): FilterOptions[] {
const filters = savedAnnotationState.getFilters(this.queryParams, this.arrayQueryParams);
if (filters === undefined) {
// return last filters if the updated filters are not yet available
return this.lastFilters;
}
this.lastFilters = filters;
return filters;
}

render(): TemplateResult {
search.updateFilters(this.filters);
return this.savedAnnotations.length > 0 ? html`
<div class="table-scroll-wrapper">
<table class="table table-index table-resource">
Expand Down
51 changes: 24 additions & 27 deletions app/assets/javascripts/components/search/dropdown_filter.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { FilterCollection, Label, FilterCollectionElement } from "components/search/filter_collection_element";
import {
Label,
FilterElement,
AccentColor
} from "components/search/filter_element";
import { i18n } from "i18n/i18n";
import { DodonaElement } from "components/meta/dodona_element";
import { FilterCollection } from "components/search/filter_collection";

/**
* This component inherits from FilterCollectionElement.
* It represents a dropdown which allows to select one or multiple labels
*
* @element d-dropdown-filter
*
* @prop {(s: Label) => string} color - a function that fetches the color associated with each label
* @prop {string} type - The type of the filter collection, used to determine the dropdown button text
* @prop {AccentColor} color - the color associated with the filter
* @prop {string} param - the searchQuery param to be used for this filter
* @prop {boolean} multi - whether one or more labels can be selected at the same time
* @prop {(l: Label) => string} paramVal - a function that extracts the value that should be used in a searchQuery for a selected label
* @prop {[Label]} labels - all labels that could potentially be selected
*/
@customElement("d-dropdown-filter")
export class DropdownFilter extends FilterCollectionElement {
@property()
color: (s: Label) => string;
@property()
type: string;
export class DropdownFilter extends FilterElement {
@property({ type: String })
color: AccentColor;

@property({ state: true })
filter = "";
Expand All @@ -35,16 +35,16 @@ export class DropdownFilter extends FilterCollectionElement {
return this.showFilter ? this.labels.filter(s => s.name.toLowerCase().includes(this.filter.toLowerCase())) : this.labels;
}

render(): TemplateResult {
if (this.labels.length === 0) {
return html``;
}
get disabled(): boolean {
return this.labels.length === 0 || (!this.multi && this.labels.length === 1);
}

render(): TemplateResult {
return html`
<div class="dropdown dropdown-filter">
<a class="token token-bordered" href="#" role="button" id="dropdownMenuLink" data-bs-toggle="dropdown" aria-expanded="false">
${this.getSelectedLabels().map( s => html`<i class="mdi mdi-circle mdi-12 mdi-colored-accent accent-${this.color(s)} left-icon"></i>`)}
${i18n.t(`js.dropdown.${this.multi?"multi":"single"}.${this.type}`)}
<a class="token token-bordered ${this.disabled ? "disabled" : ""}" href="#" role="button" id="dropdownMenuLink" data-bs-toggle="dropdown" aria-expanded="false">
${this.getSelectedLabels().map( () => html`<i class="mdi mdi-circle mdi-12 mdi-colored-accent accent-${this.color} left-icon"></i>`)}
${i18n.t(`js.search.filter.${this.param}`)}
<i class="mdi mdi-chevron-down mdi-18 right-icon"></i>
</a>

Expand All @@ -54,12 +54,13 @@ export class DropdownFilter extends FilterCollectionElement {
<input type="text" class="form-control " @input=${e => this.filter = e.target.value} placeholder="${i18n.t("js.dropdown.search")}">
</span></li>
` : ""}
${this.filteredLabels.map(s => html`
${this.filteredLabels.sort((a, b) => b.count - a.count).map(s => html`
<li><span class="dropdown-item-text ">
<div class="form-check">
<input class="form-check-input" type="${this.multi?"checkbox":"radio"}" .checked=${this.isSelected(s)} @click="${() => this.toggle(s)}" id="check-${this.param}-${s.id}">
<label class="form-check-label" for="check-${this.param}-${s.id}">
${s.name}
${s.count ? html`<span class="text-muted float-end ms-4">${s.count}</span>` : ""}
</label>
</div>
</span></li>
Expand All @@ -75,27 +76,23 @@ export class DropdownFilter extends FilterCollectionElement {
*
* @element d-dropdown-filters
*
* @prop {[string, FilterCollection][]} filterCollections - the list of filterCollections for which a dropdown should be displayed
* @prop {FilterOptions[]} filters - the list of filterOptions for which a dropdown should be displayed
* @prop {string[]} hide - the list of filter params that should be hidden
*/
@customElement("d-dropdown-filters")
export class DropdownFilters extends DodonaElement {
@property( { type: Array })
filterCollections: [string, FilterCollection][];

export class DropdownFilters extends FilterCollection {
render(): TemplateResult {
if (!this.filterCollections) {
if (!this.visibleFilters) {
return html``;
}

return html`
${this.filterCollections.map(([type, c]) => html`
${this.visibleFilters.map(c => html`
<d-dropdown-filter
.labels=${c.data}
.color=${c.color}
.paramVal=${c.paramVal}
.param=${c.param}
.multi=${c.multi}
.type=${type}
>
</d-dropdown-filter>
`)}
Expand Down
20 changes: 20 additions & 0 deletions app/assets/javascripts/components/search/filter_collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { DodonaElement } from "components/meta/dodona_element";
import { property } from "lit/decorators.js";
import { FilterOptions } from "components/search/filter_element";
import { search } from "search";

export class FilterCollection extends DodonaElement {
@property({ type: Array })
filters: FilterOptions[] = [];
@property({ type: Array })
hide: string[] = [];

constructor() {
super();
search.filterCollections.push(this);
}
jorg-vr marked this conversation as resolved.
Show resolved Hide resolved

get visibleFilters(): FilterOptions[] {
return this.filters.filter(f => !this.hide.includes(f.param));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import { searchQueryState } from "state/SearchQuery";
import { DodonaElement } from "components/meta/dodona_element";

export type Label = {id: string, name: string};
export type FilterCollection = {
export type Label = {id: string, name: string, count?: number};
export const ACCENT_COLORS = ["pink", "deep-purple", "teal", "orange", "brown", "blue-gray", "red", "purple", "indigo"];
export type AccentColor = typeof ACCENT_COLORS[number];

export type FilterOptions = {
data: Label[],
multi: boolean,
color: (l: Label) => string,
paramVal: (l: Label) => string,
color?: AccentColor,
param: string
};

Expand All @@ -21,13 +23,11 @@
* @prop {function(Label): string} paramVal - a function that extracts the value that should be used in a searchQuery for a selected label
* @prop {[Label]} labels - all labels that could potentially be selected
*/
export class FilterCollectionElement extends DodonaElement {
export class FilterElement extends DodonaElement {
@property()
param: string;
@property({ type: Boolean })
multi: boolean;
@property()
paramVal: (l: Label) => string;
@property({ type: Array })
labels: Array<Label> = [];

Expand All @@ -47,36 +47,32 @@
super.update(changedProperties);
}

private str(label: Label): string {
return this.paramVal(label).toString();
}

private get multiSelected(): string[] {
return searchQueryState.arrayQueryParams.get(this.param) || [];
}

private multiUnSelect(label: Label): void {
searchQueryState.arrayQueryParams.set(this.param, this.multiSelected.filter(s => s !== this.str(label)));
searchQueryState.arrayQueryParams.set(this.param, this.multiSelected.filter(s => s !== label.id));

Check warning on line 55 in app/assets/javascripts/components/search/filter_element.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/components/search/filter_element.ts#L55

Added line #L55 was not covered by tests
}

private multiIsSelected(label: Label): boolean {
return this.multiSelected.includes(this.str(label));
return this.multiSelected.includes(label.id);
}

private multiSelect(label: Label): void {
searchQueryState.arrayQueryParams.set(this.param, [...this.multiSelected, this.str(label)]);
searchQueryState.arrayQueryParams.set(this.param, [...this.multiSelected, label.id]);
}

private singleUnSelect(): void {
searchQueryState.queryParams.set(this.param, undefined);
}

private singleSelect(label: Label): void {
searchQueryState.queryParams.set(this.param, this.str(label));
searchQueryState.queryParams.set(this.param, label.id);
}

private singleIsSelected(label: Label): boolean {
return searchQueryState.queryParams.get(this.param) === this.str(label);
return searchQueryState.queryParams.get(this.param) === label.id;
}

isSelected = this.singleIsSelected;
Expand Down
6 changes: 2 additions & 4 deletions app/assets/javascripts/components/search/filter_tabs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { FilterCollectionElement, Label } from "components/search/filter_collection_element";
import { FilterElement, Label } from "components/search/filter_element";
import { watchMixin } from "components/meta/watch_mixin";

type TabInfo = {id: string, name: string, title?: string, count?: number};
Expand All @@ -14,13 +14,11 @@ type TabInfo = {id: string, name: string, title?: string, count?: number};
* @prop {{id: string, name: string, title: string}[]} labels - all labels that could potentially be selected
*/
@customElement("d-filter-tabs")
export class FilterTabs extends watchMixin(FilterCollectionElement) {
export class FilterTabs extends watchMixin(FilterElement) {
@property()
multi = false;
@property()
param = "tab";
@property()
paramVal = (label: Label): string => label.id.toString();
@property({ type: Array })
labels: TabInfo[];

Expand Down
25 changes: 10 additions & 15 deletions app/assets/javascripts/components/search/search_field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ import { html, TemplateResult } from "lit";
import { createDelayer } from "utilities";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { ref } from "lit/directives/ref.js";
import { FilterCollectionElement, Label } from "components/search/filter_collection_element";
import { FilterElement, Label } from "components/search/filter_element";
import { searchQueryState } from "state/SearchQuery";
import { search } from "search";
import { DodonaElement } from "components/meta/dodona_element";
import { i18n } from "i18n/i18n";
import { FilterCollection } from "components/search/filter_collection";
/**
* This component inherits from FilterCollectionElement.
* It represents a list of filters to be used in a dropdown as typeahead suggestions
*
* @element d-search-field-suggestion
*
* @prop {string} type - The type of the filter collection, used to determine the dropdown button text
* @prop {string} filter - The string for which typeahead suggestions should be provided
* @prop {number} index - bookkeeping param to remember the order across multiple elements
* @prop {string} param - the searchQuery param to be used for this filter
Expand All @@ -23,9 +22,7 @@ import { i18n } from "i18n/i18n";
* @prop {[Label]} labels - all labels that could potentially be selected
*/
@customElement("d-search-field-suggestion")
export class SearchFieldSuggestion extends FilterCollectionElement {
@property()
type: string;
export class SearchFieldSuggestion extends FilterElement {
jorg-vr marked this conversation as resolved.
Show resolved Hide resolved
@property()
filter: string;
@property({ type: Number })
Expand Down Expand Up @@ -54,11 +51,12 @@ export class SearchFieldSuggestion extends FilterCollectionElement {
render(): TemplateResult {
return this.getFilteredLabels().length == 0 ? html`` : html`
<li>
<h6 class='dropdown-header'>${i18n.t(`js.${this.type}`)}</h6>
<h6 class='dropdown-header'>${i18n.t(`js.search.filter.${this.param}`)}</h6>
</li>
${ this.getFilteredLabels().map( label => html`
<li><a class="dropdown-item" href="#" @click=${e => this.handleClick(e, label)}>
${unsafeHTML(this.getHighlightedLabel(label.name))}
${label.count ? html`<span class="text-muted float-end ms-4">${label.count}</span>` : ""}
</a></li>
`)}
`;
Expand All @@ -73,17 +71,16 @@ export class SearchFieldSuggestion extends FilterCollectionElement {
*
* @prop {string} placeholder - The placeholder text for an empty searchfield
* @prop {boolean} eager - if true a search will be run before user input happens
* @prop {Record<string, { data: Label[], multi: boolean, paramVal: (l: Label) => string, param: string }>} filterCollections
* @prop {FilterOptions[]} filters - The list of filter lists to be used as search suggestions
* @prop {string[]} hide - The list of filter lists to be ignored as search suggestions
* - The list of filter lists to be used as search suggestions
*/
@customElement("d-search-field")
export class SearchField extends DodonaElement {
export class SearchField extends FilterCollection {
@property({ type: String })
placeholder: string;
@property({ type: Boolean })
eager: boolean;
@property( { type: Array })
filterCollections: Record<string, { data: Label[], multi: boolean, paramVal: (l: Label) => string, param: string }>;

@property({ state: true })
filter?: string = "";
Expand Down Expand Up @@ -150,7 +147,7 @@ export class SearchField extends DodonaElement {
}

render(): TemplateResult {
if (!this.filterCollections) {
if (!this.visibleFilters) {
return html``;
}

Expand All @@ -166,12 +163,10 @@ export class SearchField extends DodonaElement {
@keydown=${e => this.keydown(e)}
/>
<ul class="dropdown-menu ${this.filter && this.hasSuggestions ? "show-search-dropdown" : ""}">
${Object.entries(this.filterCollections).map(([type, c], i) => html`
${this.visibleFilters.map((c, i) => html`
<d-search-field-suggestion
.labels=${c.data}
.type=${type}
.filter=${this.filter}
.paramVal=${c.paramVal}
.param=${c.param}
.multi=${c.multi}
.index=${i}
Expand Down
Loading