Skip to content

Commit

Permalink
Merge pull request #5430 from dodona-edu/enhance/search-action-discov…
Browse files Browse the repository at this point in the history
…erability

Make search actions and options more discoverable
  • Loading branch information
jorg-vr authored Apr 22, 2024
2 parents fbc6eac + 787d08e commit 11e4aee
Show file tree
Hide file tree
Showing 26 changed files with 298 additions and 271 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class DropdownFilter extends FilterCollectionElement {

return html`
<div class="dropdown dropdown-filter">
<a class="btn btn-outline dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-bs-toggle="dropdown" aria-expanded="false">
<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}`)}
<i class="mdi mdi-chevron-down mdi-18 right-icon"></i>
Expand Down
7 changes: 5 additions & 2 deletions app/assets/javascripts/components/search/filter_tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { customElement, property } from "lit/decorators.js";
import { FilterCollectionElement, Label } from "components/search/filter_collection_element";
import { watchMixin } from "components/meta/watch_mixin";

type TabInfo = {id: string, name: string, title?: string, count?: number};

/**
* This component inherits from FilterCollectionElement.
* It represent a list of tabs, where each tab shows a filtered set of the search results
Expand All @@ -20,7 +22,7 @@ export class FilterTabs extends watchMixin(FilterCollectionElement) {
@property()
paramVal = (label: Label): string => label.id.toString();
@property({ type: Array })
labels: {id: string, name: string, title: string}[];
labels: TabInfo[];

processClick(e: Event, label: Label): void {
if (!this.isSelected(label)) {
Expand All @@ -42,9 +44,10 @@ export class FilterTabs extends watchMixin(FilterCollectionElement) {
<div class="card-tab">
<ul class="nav nav-tabs" role="tablist">
${this.labels.map(label => html`
<li role="presentation" data-bs-toggle="tooltip" data-bs-title="${label.title ? label.title : ""}" data-bs-trigger="hover">
<li role="presentation" data-bs-toggle="tooltip" title="${label.title ? label.title : ""}" data-bs-trigger="hover">
<a href="#" @click=${e => this.processClick(e, label)} class="${this.isSelected(label) ? "active" : ""}">
${label.name}
${label.count ? html`<span class="badge rounded-pill colored-secondary" id="${label.id}-count">${label.count}</span>` : ""}
</a>
</li>
`)}
Expand Down
11 changes: 10 additions & 1 deletion app/assets/javascripts/components/search/loading_bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { search } from "search";
import { DodonaElement } from "components/meta/dodona_element";
import { watchMixin } from "components/meta/watch_mixin";

/**
* This component represents a loading bar.
Expand All @@ -10,7 +11,7 @@ import { DodonaElement } from "components/meta/dodona_element";
* @element d-loading-bar
*/
@customElement("d-loading-bar")
export class LoadingBar extends DodonaElement {
export class LoadingBar extends watchMixin(DodonaElement) {
@property({ type: Boolean, attribute: "search-based" })
searchBased = false;

Expand All @@ -24,6 +25,14 @@ export class LoadingBar extends DodonaElement {
}
}

watch = {
searchBased: () => {
if (this.searchBased) {
search.loadingBars.push(this);
}
}
};

show(): void {
this.loading = true;
}
Expand Down
124 changes: 21 additions & 103 deletions app/assets/javascripts/components/search/search_actions.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,19 @@
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Toast } from "toast";
import { fetch, ready } from "utilities";
import { fetch } from "utilities";
import { searchQueryState } from "state/SearchQuery";
import { DodonaElement } from "components/meta/dodona_element";
import { i18n } from "i18n/i18n";

export type SearchOption = {search: Record<string, string>, type?: string, text: string};
export type SearchAction = {
url?: string,
type?: string,
filterValue?: string,
text: string,
action?: string,
js?: string,
confirm?: string,
icon: string
};

const isSearchOption = (opt): opt is SearchOption => (opt as SearchOption).search !== undefined;
const isSearchAction = (act): act is SearchAction => (act as SearchAction).js !== undefined || (act as SearchAction).action !== undefined || (act as SearchAction).url !== undefined;

/**
* This component represents a SearchOption using a checkbox to be used in a dropdown list
* The checkbox tracks whether the searchoption is curently active
*
* @element d-search-option
*
* @prop {SearchOption} searchOption - the search option which can be activated or disabled
* @prop {number} key - unique identifier used to differentiate from other search options
*/
@customElement("d-search-option")
export class SearchOptionElement extends DodonaElement {
@property({ type: Object })
searchOption: SearchOption;
@property( { type: Number })
key: number;

get active(): boolean {
return Object.entries(this.searchOption.search).every(([key, value]) => {
return searchQueryState.queryParams.get(key) == value.toString();
});
}

performSearch(): void {
if (!this.active) {
Object.entries(this.searchOption.search).forEach(([key, value]) => {
searchQueryState.queryParams.set(key, value.toString());
});
} else {
Object.keys(this.searchOption.search).forEach(key => {
searchQueryState.queryParams.set(key, undefined);
});
}
}

render(): TemplateResult {
return html`
<li><span class="dropdown-item-text ">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
.checked=${this.active}
@click="${() => this.performSearch()}"
id="check-${this.searchOption.type}-${this.key}"
>
<label class="form-check-label" for="check-${this.searchOption.type}-${this.key}">
${this.searchOption.text}
</label>
</div>
</span></li>
`;
}
}

/**
* This component represents a dropdown containing a combination of SearchOptions and SearchActions
*
Expand All @@ -84,15 +24,9 @@ export class SearchOptionElement extends DodonaElement {
@customElement("d-search-actions")
export class SearchActions extends DodonaElement {
@property({ type: Array })
actions: (SearchOption|SearchAction)[] = [];

getSearchOptions(): Array<SearchOption> {
return this.actions.filter(isSearchOption);
}

getSearchActions(): Array<SearchAction> {
return this.actions.filter(isSearchAction);
}
actions: SearchAction[] = [];
@property({ type: String, attribute: "filter-param" })
filterParam = undefined;

async performAction(action: SearchAction): Promise<boolean> {
if (!action.action && !action.js && !action.url) {
Expand Down Expand Up @@ -130,39 +64,23 @@ export class SearchActions extends DodonaElement {
return false;
}

get filteredActions(): SearchAction[] {
if (!this.filterParam) {
return this.actions;
}

render(): TemplateResult {
return html`
<div class="dropdown actions" id="kebab-menu">
<a class="btn btn-icon dropdown-toggle" data-bs-toggle="dropdown">
<i class="mdi mdi-dots-vertical"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end">
${this.getSearchOptions().length > 0 ? html`
<li><h6 class='dropdown-header'>${i18n.t("js.options")}</h6></li>
` : html``}
${this.getSearchOptions().map((opt, id) => html`
<d-search-option .searchOption=${opt}
.key=${id}>
</d-search-option>
`)}
const filterValue = searchQueryState.queryParams.get(this.filterParam);
return this.actions.filter(action => action.filterValue === undefined || action.filterValue === filterValue);
}

${this.getSearchActions().length > 0 ? html`
<li><h6 class='dropdown-header'>${i18n.t("js.actions")}</h6></li>
` : html``}
${this.getSearchActions().map(action => html`
<li>
<a class="action dropdown-item"
data-type="${action.type}"
@click=${() => this.performAction(action)}
>
<i class='mdi mdi-${action.icon} mdi-18'></i>
${action.text}
</a>
</li>
`)}
</ul>
</div>
`;
render(): TemplateResult[] {
return this.filteredActions.map(action => html`
<a class="btn btn-outline with-icon ml-2"
@click=${() => this.performAction(action)}
>
<i class='mdi mdi-${action.icon} mdi-18'></i>
${action.text}
</a>
`);
}
}
88 changes: 88 additions & 0 deletions app/assets/javascripts/components/search/search_option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { customElement, property } from "lit/decorators.js";
import { DodonaElement } from "components/meta/dodona_element";
import { searchQueryState } from "state/SearchQuery";
import { html, TemplateResult } from "lit";
import { i18n } from "i18n/i18n";

export type Option = {param: string, label: string};

/**
* This component represents a boolean option for search using a checkbox
* The checkbox tracks whether the search option is curently active
*
* @element d-search-option
*
* @prop {string} param - the name of the search parameter
* @prop {string} label - the label to be displayed next to the checkbox
*/
@customElement("d-search-option")
export class SearchOption extends DodonaElement {
@property({ type: String })
param = "";
@property({ type: String })
label = "";

get active(): boolean {
return searchQueryState.queryParams.get(this.param) !== undefined;
}

toggle(): void {
if (this.active) {
searchQueryState.queryParams.set(this.param, undefined);
} else {
searchQueryState.queryParams.set(this.param, "true");
}
}

render(): TemplateResult {
return html`
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
.checked=${this.active}
@click="${() => this.toggle()}"
id="check-${this.param}"
>
<label class="form-check-label" for="check-${this.param}">
${this.label}
</label>
</div>
`;
}
}

/**
* This component represents a list op boolean options for search
* The options are displayed in a dropdown
*
* @element d-search-options
*
* @prop {Option[]} options - the list of options to be displayed
*/
@customElement("d-search-options")
export class SearchOptions extends DodonaElement {
@property({ type: Array })
options: Option[] = [];

render(): TemplateResult {
if (this.options.length === 0) {
return html``;
}

return html`
<div class="dropdown">
<a class="btn btn-icon dropdown-toggle" data-bs-toggle="dropdown">
<i class="mdi mdi-dots-vertical"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end">
${this.options.map(o => html`
<li><span class="dropdown-item-text ">
<d-search-option param="${o.param}" label="${o.label}"></d-search-option>
</span></li>
`)}
</ul>
</div>
`;
}
}
4 changes: 3 additions & 1 deletion app/assets/javascripts/components/search/search_token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export class SearchToken extends FilterCollectionElement {
${ this.getSelectedLabels().map( label => html`
<div class="token accent-${this.color(label)}">
<span class="token-label">${label.name}</span>
<a href="#" class="close" tabindex="-1" @click=${e => this.processClick(e, label)}>×</a>
<a href="#" class="close" tabindex="-1" @click=${e => this.processClick(e, label)}>
<i class="mdi mdi-close mdi-18"></i>
</a>
</div>
`)}
`;
Expand Down
Loading

0 comments on commit 11e4aee

Please sign in to comment.