Skip to content

Commit

Permalink
fix: sort by relevance on initial search
Browse files Browse the repository at this point in the history
1. On initial page load, codebases are sorted by date: newest.
2. On first search, results are sorted by relevance
3. Disable buttons in the sidebar when loading
4. Disable "Apply filters" button if filters are unchanged
  • Loading branch information
asuworks authored and alee committed Aug 30, 2024
1 parent 67b108d commit 42c2760
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 162 deletions.
13 changes: 6 additions & 7 deletions django/core/jinja2/common.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@
{% for term in list_state.filter_display_terms %}
<span class="badge bg-gray ms-1">{{ term }}</span>
{% endfor %}
<a class="text-danger ms-1" href="{{ url(url_name) }}">
<i class="fas fa-times"></i> clear
<a class="text-warn ms-1" href="{{ url(url_name) }}">
<i class="fas fa-times"></i> clear filters
</a>
{% endif %}
</p>
Expand All @@ -189,11 +189,10 @@
<div class="input-group">
<input id="search-query" class="form-control" name="query" type="search" value="{{ current_query }}"
placeholder="{{ placeholder }}">
{% if query_params %}
{% for key, value in generate_hidden_inputs(query_params) %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}
{% endif %}
{% for key, value in generate_hidden_inputs(query_params) %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}

<button type="submit" class="btn btn-primary">
<i class="fas fa-search px-1"></i>
</button>
Expand Down
9 changes: 9 additions & 0 deletions django/core/jinja_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,18 @@ def generate_hidden_inputs(query_params):
if query_params:
# parse_qsl handles splitting and unquoting key-value pairs
parsed_params = parse_qsl(query_params)

# set default ordering, if it is not specified
if not any(key == "ordering" for key, value in parsed_params):
hidden_inputs.append(("ordering", "relevance"))

for key, value in convert_keys_to_camel_case(parsed_params):
if key != "query":
hidden_inputs.append((key, value))
else:
# initial ordering of the codebase search results
hidden_inputs.append(("ordering", "relevance"))

return hidden_inputs


Expand Down
4 changes: 2 additions & 2 deletions django/core/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ def create_paginated_context_data(
Args:
query: request query
data: results from Elasticsearch
page: requested page
page: requested page (typically from http query params)
count: total number of results
query_params: query dictionary
size (optional): number of results per page (default=cls.page_size)
size (optional): number of results per page, defaults to cls.page_size (currently: 10)
Returns:
dict: paginated context data
Expand Down
4 changes: 4 additions & 0 deletions django/library/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,10 @@ def filter_queryset(self, request, queryset, view):
criteria.update(id__in=filtered_codebase_ids)
if ordering:
criteria.update(ordering=ordering)
else:
if qs:
# set default ordering for search when ordering is not specified
criteria.update(ordering="relevance")

return get_search_queryset(qs, queryset, tags=tags, criteria=criteria)

Expand Down
216 changes: 76 additions & 140 deletions frontend/src/components/CodebaseListSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,11 @@
create-url="/codebases/add/"
search-label="Apply Filters"
:clear-all-filters="clearAllFilters"
:is-filter-changed="isFilterChanged"
:search-url="query"
>
<template #form>
<form @submit.prevent="handleSubmit">
<div class="mb-3" v-if="selectedFilters.length > 0">
<label class="form-label fw-bold"
>Selected Filters
<a @click="clearAllFilters" class="p-2" aria-label="clear all">Clear all</a>
</label>
<div class="d-flex flex-wrap gap-2">
<span
v-for="filter in selectedFilters"
:key="filter.key"
class="badge bg-light text-dark text-wrap d-flex align-items-center"
>
{{ filter.label }}
<button
@click="removeFilter(filter.key)"
class="btn-close ms-2"
aria-label="Close"
></button>
</span>
</div>
<hr class="hr" />
</div>

<div class="mb-3">
<label class="form-label fw-bold">Peer Review Status</label>
<div v-for="option in peerReviewOptions" :key="option.value" class="form-check">
Expand All @@ -39,18 +18,15 @@
:id="option.value"
:value="option.value"
v-model="values.peerReviewStatus"
@change="updateFilters"
/>
<label class="form-check-label" :for="option.value">
{{ option.label }}
</label>
</div>
</div>

<div class="mb-3">
<label v-if="parsedLanguageFacets.length > 0" class="form-label fw-bold">
Programming Languages
</label>
<div class="mb-3" v-if="parsedLanguageFacets.length > 0">
<label class="form-label fw-bold"> Programming Languages </label>
<div class="row">
<div v-for="lang in parsedLanguageFacets" :key="lang.value" class="col-12 col-md-12">
<div class="form-check">
Expand All @@ -60,7 +36,6 @@
:id="lang.value"
:value="lang.value"
v-model="values.programmingLanguages"
@change="updateFilters"
/>
<label class="form-check-label" :for="lang.value">
{{ lang.label }}
Expand All @@ -70,13 +45,13 @@
</div>
</div>

<div class="row mb-4 fw-bold">
<DatepickerField name="startDate" label="Published After" class="col-12 col-md-6" />
<DatepickerField name="endDate" label="Published Before" class="col-12 col-md-6" />
<div class="row mb-3 fw-bold">
<DatepickerField name="startDate" label="Published After" class="col-12 col-md-12 py-2" />
<DatepickerField name="endDate" label="Published Before" class="col-12 col-md-12 py-2" />
</div>

<TaggerField
class="mb-3"
class="mb-1"
name="tags"
label="Tags"
type="Codebase"
Expand All @@ -89,36 +64,14 @@

<script setup lang="ts">
import * as yup from "yup";
import { onMounted, computed, ref, watch } from "vue";
import { onMounted, computed } from "vue";
import { defineProps } from "vue";
import ListSidebar from "@/components/ListSidebar.vue";
import DatepickerField from "@/components/form/DatepickerField.vue";
import TaggerField from "@/components/form/TaggerField.vue";
import { useForm } from "@/composables/form";
import { useCodebaseAPI } from "@/composables/api";
const props = defineProps<{
languageFacets?: Record<string, number>;
}>();
// Define a variable to store the parsed language facets
let parsedLanguageFacets: { value: string; label: string }[] = [];
onMounted(() => {
if (props.languageFacets) {
const localLanguageFacets = { ...props.languageFacets };
parsedLanguageFacets = Object.entries(localLanguageFacets)
.sort(([, valueA], [, valueB]) => valueB - valueA) // Sort by value in descending order
.map(([name, value]) => ({ value: name, label: `${name} (${value})` }));
} else {
console.warn("languageFacets is undefined");
}
initializeFilters();
watch([() => values.startDate, () => values.endDate, () => values.tags], updateFilters);
});
const peerReviewOptions = [
{ value: "reviewed", label: "Reviewed" },
{ value: "not_reviewed", label: "Not Reviewed" },
Expand All @@ -139,87 +92,72 @@ const schema = yup.object({
});
type SearchFields = yup.InferType<typeof schema>;
const { handleSubmit, values } = useForm<SearchFields>({
schema,
initialValues: {
peerReviewStatus: "",
programmingLanguages: [],
tags: [],
startDate: null,
endDate: null,
ordering: "",
},
initialValues: {},
onSubmit: () => {
window.location.href = query.value;
},
});
const { searchUrl } = useCodebaseAPI();
const selectedFilters = ref<Array<{ key: string; label: string }>>([]);
const updateFilters = () => {
selectedFilters.value = [
// Peer Review Status
...(values.peerReviewStatus
? [
{
key: `peerReview_${values.peerReviewStatus}`,
label: `Peer Review: ${
peerReviewOptions.find(o => o.value === values.peerReviewStatus)?.label ||
values.peerReviewStatus
}`,
},
]
: []),
// Programming Languages
...(values.programmingLanguages && values.programmingLanguages.length > 0
? values.programmingLanguages.map(lang => ({
key: `lang_${lang}`,
label: `Language: ${
parsedLanguageFacets.find(l => l.value === lang)?.value || `${lang}`
}`,
}))
: []),
// Start Date
...(values.startDate
? [{ key: "startDate", label: `After: ${values.startDate.toLocaleDateString()}` }]
: []),
type LanguageFacet = {
value: string;
label: string;
};
const props = defineProps<{
languageFacets?: Record<string, number>;
}>();
// End Date
...(values.endDate
? [{ key: "endDate", label: `Before: ${values.endDate.toLocaleDateString()}` }]
: []),
let parsedLanguageFacets: LanguageFacet[] = [];
// Tags
...(values.tags && values.tags.length > 0
? values.tags.map(tag => ({
key: `tag_${tag.name}`,
label: `Tag: ${tag.name}`,
}))
: []),
];
const initialFilterValues = {
value: { ...values },
};
const removeFilter = (key: string) => {
if (key.startsWith("peerReview_")) {
values.peerReviewStatus = "";
} else if (key.startsWith("lang_")) {
values.programmingLanguages =
values.programmingLanguages?.filter(lang => `lang_${lang}` !== key) || [];
} else if (key === "startDate") {
values.startDate = null;
} else if (key === "endDate") {
values.endDate = null;
} else if (key.startsWith("tag_")) {
const tagName = key.slice(4); // Remove 'tag_' prefix
values.tags = values.tags?.filter(tag => tag.name !== tagName) || [];
onMounted(() => {
if (props.languageFacets) {
const localLanguageFacets = { ...props.languageFacets };
console.log(localLanguageFacets);
parsedLanguageFacets = Object.entries(localLanguageFacets)
.sort(([, valueA], [, valueB]) => valueB - valueA) // Sort by value in descending order
.map(([name, value]) => ({ value: name, label: `${name} (${value})` }));
} else {
console.warn("languageFacets is undefined");
}
updateFilters();
initializeFilterValues();
});
const initializeFilterValues = () => {
const urlParams = new URLSearchParams(window.location.search);
values.peerReviewStatus = urlParams.get("peerReviewStatus") || "";
values.programmingLanguages = urlParams.getAll("programmingLanguages").sort() || [];
values.tags = urlParams.getAll("tags").map(tag => ({ name: tag })) || [];
values.startDate = urlParams.get("publishedAfter")
? new Date(urlParams.get("publishedAfter")!)
: null;
values.endDate = urlParams.get("publishedBefore")
? new Date(urlParams.get("publishedBefore")!)
: null;
values.ordering = urlParams.get("ordering") || "-first_published_at";
initialFilterValues.value = { ...values };
};
const isFilterChanged = computed(() => {
const currentValues = {
...values,
programmingLanguages: sortedProgrammingLanguages.value, // Use sorted values for comparison
};
return !deepEqual(currentValues, initialFilterValues.value);
});
// Computed property for sorted programming languages
const sortedProgrammingLanguages = computed(() => {
return values.programmingLanguages?.slice().sort(); // Sort the programming languages
});
const query = computed(() => {
const url = new URLSearchParams(window.location.search);
const searchQuery = url.get("query") ?? "";
Expand All @@ -235,34 +173,32 @@ const query = computed(() => {
});
});
const initializeFilters = () => {
const urlParams = new URLSearchParams(window.location.search);
values.peerReviewStatus = urlParams.get("peerReviewStatus") || "";
values.programmingLanguages = urlParams.getAll("programmingLanguages") || [];
values.tags = urlParams.getAll("tags").map(tag => ({ name: tag })) || [];
values.startDate = urlParams.get("publishedAfter")
? new Date(urlParams.get("publishedAfter")!)
: null;
values.endDate = urlParams.get("publishedBefore")
? new Date(urlParams.get("publishedBefore")!)
: null;
values.ordering = urlParams.get("ordering") || "-first_published_at";
updateFilters();
};
const clearAllFilters = () => {
values.peerReviewStatus = "";
values.programmingLanguages = [];
values.tags = [];
values.startDate = null;
values.endDate = null;
values.ordering = "-first_published_at";
updateFilters();
window.location.href = query.value;
};
defineExpose({ clearAllFilters });
function deepEqual(obj1: any, obj2: any): boolean {
if (obj1 === obj2) return true; // Same reference or both are null/undefined
if (typeof obj1 !== "object" || obj1 === null || typeof obj2 !== "object" || obj2 === null)
return false;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
// Check if the key exists in both objects and deeply compare the values
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) return false;
}
return true;
}
</script>
Loading

0 comments on commit 42c2760

Please sign in to comment.