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

Make search actions and options more discoverable #5430

Merged
merged 16 commits into from
Apr 22, 2024
Merged
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
127 changes: 23 additions & 104 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 @@
@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) {
Expand Down Expand Up @@ -124,40 +58,25 @@
return false;
}

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

Check warning on line 63 in app/assets/javascripts/components/search/search_actions.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/components/search/search_actions.ts#L63

Added line #L63 was not covered by tests
}

const filterValue = searchQueryState.queryParams.get(this.filterParam);
return this.actions.filter(action => action.filterValue === undefined || action.filterValue === filterValue);
}

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>
`)}

${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"
href='${action.url ? action.url : "#"}'
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 m-2 me-0"
href='${action.url ? action.url : "#"}'
@click=${() => this.performAction(action)}
>
<i class='mdi mdi-${action.icon} mdi-18'></i>
${action.text}
</a>
`);
}
}
94 changes: 94 additions & 0 deletions app/assets/javascripts/components/search/search_option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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";

Check warning on line 5 in app/assets/javascripts/components/search/search_option.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/components/search/search_option.ts#L1-L5

Added lines #L1 - L5 were not covered by tests

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 {

Check warning on line 29 in app/assets/javascripts/components/search/search_option.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/components/search/search_option.ts#L29

Added line #L29 was not covered by tests
if (this.active) {
searchQueryState.queryParams.set(this.param, undefined);
} else {
searchQueryState.queryParams.set(this.param, "true");

Check warning on line 33 in app/assets/javascripts/components/search/search_option.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/components/search/search_option.ts#L31-L33

Added lines #L31 - L33 were not covered by tests
}
}

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

Check warning on line 44 in app/assets/javascripts/components/search/search_option.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/components/search/search_option.ts#L44

Added line #L44 was not covered by tests
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[] = [];

get activeOptions(): Option[] {
return this.options.filter(option => searchQueryState.queryParams.get(option.param) !== undefined);
}

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

Check warning on line 74 in app/assets/javascripts/components/search/search_option.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/components/search/search_option.ts#L74

Added line #L74 was not covered by tests
}

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">
${this.activeOptions.map( () => html`<i class="mdi mdi-circle mdi-12 mdi-colored-accent accent-gray left-icon"></i>`)}

Check warning on line 80 in app/assets/javascripts/components/search/search_option.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/components/search/search_option.ts#L80

Added line #L80 was not covered by tests
${i18n.t(`js.dropdown.multi.search_options`)}
<i class="mdi mdi-chevron-down mdi-18 right-icon"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuLink">
${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>
`;
}
}
67 changes: 0 additions & 67 deletions app/assets/javascripts/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,65 +13,8 @@ function loadUsers(_status = undefined): void {

function initCourseMembers(): void {
function init(): void {
initUserTabs();
initLabelsEditModal();
}

function initUserTabs(): void {
const userTabs = document.getElementById("user-tabs");
if (userTabs === null) {
return;
}

const baseUrl = userTabs.dataset.baseurl;
// Select tab and load users
const selectTab = (tab): HTMLElement => {
const kebab = document.getElementById("kebab-menu");
const status = tab.dataset.status;
const kebabItems = kebab.querySelectorAll<HTMLElement>("li a.action");
let anyShown = false;
for (const item of kebabItems) {
const dataType = item.dataset.type;
if (dataType && dataType !== status) {
hideElement(item);
} else {
showElement(item);
anyShown = true;
}
}
if (anyShown) {
showElement(kebab);
} else {
hideElement(kebab);
}
if (tab.parentNode.classList.contains("active")) {
// The current tab is already loaded, nothing to do
return;
}

loadUsers(status);
document.querySelector("#user-tabs li.active").classList.remove("active");
tab.parentNode.classList.add("active");
};

// Switch to clicked tab
document.querySelectorAll("#user-tabs li a")
.forEach(el => {
el.addEventListener("click", function (e) {
selectTab(el);
e.preventDefault();
});
});

// Determine which tab to show first
const status = searchQueryState.queryParams.get("status");
let tab = document.querySelector("a[data-status='" + status + "']");
if (tab === null) {
tab = document.querySelector("a[data-status='enrolled']");
}
selectTab(tab);
}

function initLabelsEditModal(): void {
document.getElementById("labelsUploadButton").addEventListener("click", () => {
const modal = document.getElementById("labelsUploadModal");
Expand Down Expand Up @@ -99,19 +42,9 @@ function initCourseMembers(): void {
});
}

function hideElement(element: HTMLElement): void {
element.style.display = "none";
}

function showElement(element: HTMLElement): void {
element.style.display = "block";
}

init();
}

const ICON_SELECTOR = ".series-icon";

class Series {
public readonly id: number;
private url: string;
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/i18n/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
"programming_languages": "Programming Languages",
"question_states": "Statuses",
"repositories": "Repositories",
"search_options": "Options",
"statuses": "Statuses"
},
"search": "Search",
Expand Down Expand Up @@ -726,6 +727,7 @@
"programming_languages": "Programmeertalen",
"question_states": "Toestanden",
"repositories": "Repository's",
"search_options": "Opties",
"statuses": "Statussen"
},
"search": "Zoeken",
Expand Down
4 changes: 0 additions & 4 deletions app/assets/stylesheets/components/table-toolbar.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
color: var(--d-on-surface);
margin-bottom: 6px;

.btn {
font-size: 18px;
}

.dropdown-filters {
margin-left: 8px;
}
Expand Down
Loading