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 GitHub-style filter for columns (fix #37) #58

Merged
merged 32 commits into from
Mar 25, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5428fa0
Separate data of table from its UI handling
Ethkuil Feb 10, 2023
1089205
Add GitHub-style filter for columns
Ethkuil Feb 10, 2023
62e94c0
Adjust position of filter box
Ethkuil Feb 13, 2023
2318d5f
Apply change in filter field automatically
Ethkuil Feb 13, 2023
1967f20
Edit the placeholder of filter field
Ethkuil Feb 13, 2023
e3fc24a
Fix wrong message display after filtering
Ethkuil Feb 23, 2023
c51abe2
Update the way to deal with invalid filter pattern
Ethkuil Feb 23, 2023
2a8616d
Fix logic to parse filter pattern
Ethkuil Feb 26, 2023
120a681
Replace "Export table as CSV" with "Export table below as CSV"
Ethkuil Feb 26, 2023
9c446d3
Add single-letter aliases for each column
Ethkuil Mar 1, 2023
0ca2586
Fix bug of overwriting error msg with empty msg
Ethkuil Mar 5, 2023
c511135
Show and hide filter field automatically
Ethkuil Mar 6, 2023
6b2f980
Apply little refactor and formatting
Ethkuil Mar 7, 2023
985843b
Fix small formatting issue
payne911 Mar 7, 2023
26cff66
Reducing area of "git blame"
payne911 Mar 7, 2023
96c7c83
Add some more aliases
payne911 Mar 15, 2023
f7363da
Tooltip for the search field
payne911 Mar 15, 2023
c296c9b
Fix incompatibility with progress bar display
Ethkuil Mar 15, 2023
199c24d
Separete scaning state msgs from non-error msgs
Ethkuil Mar 16, 2023
dd898a8
Make the filter function global
Ethkuil Mar 17, 2023
86d555a
style
payne911 Mar 22, 2023
229d0ce
Avoid querying a duplicated result (resolve #59)
payne911 Mar 22, 2023
909e35a
Remove the ahead filter from the settings
payne911 Mar 22, 2023
9217b48
Less emphasis on the filter field
payne911 Mar 22, 2023
ad2ea73
Dynamically adjust the appearance of the filter field
payne911 Mar 22, 2023
2d25117
Fix filter update
Ethkuil Mar 22, 2023
52e3d8c
Adding a comment
payne911 Mar 23, 2023
bb44d78
Change filtering syntax to be simpler
payne911 Mar 24, 2023
610fabd
Extract constants out of for-loops
payne911 Mar 24, 2023
dd064ba
Extract repeated value for better maintainability
payne911 Mar 24, 2023
ede116b
Extract the date regex
payne911 Mar 24, 2023
b6c6312
Update date regex to be more robust
payne911 Mar 25, 2023
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
7 changes: 5 additions & 2 deletions website/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@
Display
</label>
</div>
<p class="help mb-2"><strong>Default is checked.</strong> Determines whether to display or not the "<span class="is-family-monospace">Export table as CSV</span>" button when a scan ends.</p>
<p class="help mb-2"><strong>Default is checked.</strong> Determines whether to display or not the "<span class="is-family-monospace">Export table below as CSV</span>" button when a scan ends.</p>
</div>
</div>
</div>
Expand Down Expand Up @@ -230,7 +230,10 @@
Find useful forks
</button>
</p>
</div>
</div>
Ethkuil marked this conversation as resolved.
Show resolved Hide resolved
<div class="control">
<input class="input is-dark" type="text" id="filter" name="filter" placeholder="Type your filter here. eg: ahead:>=1 behind:<5" />
</div>
</div>
</div>
<div class="container" id="useful_forks_inject">
Expand Down
12 changes: 4 additions & 8 deletions website/src/csv-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,22 @@ const EXPORT_DIV_BTN = "uf_csv_export_btn";
const EXPORT_BTN = $('<a>', {id: EXPORT_DIV_BTN, class: "button is-dark is-outlined is-small"})
.attr("href", "#")
.prepend($('<img>', {src: "assets/csv_dl.svg", class: "mr-2", alt: "csv", width: "26", height: "26"}))
.append("Export table as CSV");
.append("Export table below as CSV");
const EXPORT_DIV = $('<div>', {id: EXPORT_DIV_ID, class: "mt-2"}).append(EXPORT_BTN);


function displayCsvExportBtn() {
if (!UF_SETTINGS_CSV_DISPLAY) {
return;
}
if (!tableIsEmpty()) {
JQ_ID_HEADER.append(EXPORT_DIV);
setClickEvent();
}
JQ_ID_HEADER.append(EXPORT_DIV);
setClickEvent();
}
function hideExportCsvBtn() {
if (!UF_SETTINGS_CSV_DISPLAY) {
return;
}
if (!tableIsEmpty()) {
getJqId_$(EXPORT_DIV_ID).remove();
}
getJqId_$(EXPORT_DIV_ID).remove();
}
function setClickEvent() {
EXPORT_BTN.on('click', function (event) {
Expand Down
14 changes: 13 additions & 1 deletion website/src/queries-init.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const SELF_URL = "https://useful-forks.github.io/";

const JQ_REPO_FIELD = $('#repo');
const JQ_FILTER_FIELD = $('#filter');
const JQ_SEARCH_BTN = $('#searchBtn');
const JQ_TOTAL_CALLS = $('#totalApiCalls');

Expand All @@ -21,7 +22,6 @@ const UF_MSG_API_RATE = "<b>GitHub API rate-limits exceeded.</b> Consider pr
// list of messages which should not be cleared when the request ends
const UF_PRESERVED_MSGS = [
UF_MSG_NO_FORKS,
UF_MSG_EMPTY_FILTER,
Ethkuil marked this conversation as resolved.
Show resolved Hide resolved
UF_MSG_ERROR,
UF_MSG_API_RATE
];
Expand Down Expand Up @@ -102,6 +102,11 @@ function setMsg(msg) {
.css("border-color", "rgba(0,0,0,0.25)")
.css("border-style", "solid");
}

function isMsgEmpty() {
return JQ_ID_MSG.html() === "";
}

function clearMsg() {
JQ_ID_MSG
.empty()
Expand Down Expand Up @@ -151,6 +156,13 @@ function getQueryOrDefault(defaultVal) {
return JQ_REPO_FIELD.val();
}

function getFilterOrDefault(defaultVal) {
if (!JQ_FILTER_FIELD.val()) {
JQ_FILTER_FIELD.val(defaultVal);
}
return JQ_FILTER_FIELD.val();
}

function setApiCallsLabel(total) {
JQ_TOTAL_CALLS.html(total + " calls");
}
Expand Down
216 changes: 165 additions & 51 deletions website/src/queries-logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { Octokit } = require("@octokit/rest");
const { throttling } = require("@octokit/plugin-throttling");

/* Variables that should be cleared for every new query (defaults are set in "clear_old_data"). */
let TABLE_DATA = [];
let REPO_DATE;
let TOTAL_FORKS;
let RATE_LIMIT_EXCEEDED;
Expand All @@ -13,7 +14,8 @@ let ONGOING_REQUESTS_COUNTER = 0;
function clear_old_data() {
clearHeader();
clearMsg();
clearTable();
TABLE_DATA = []; // clear the table data
clearTable(); // clear the table DOM
setApiCallsLabel(0);
hideExportCsvBtn();
REPO_DATE = new Date();
Expand Down Expand Up @@ -135,9 +137,19 @@ function allRequestsAreDone() {
function decrementCounters() {
ONGOING_REQUESTS_COUNTER--;
if (allRequestsAreDone()) {
clearNonErrorMsg();
sortTable();
manageMsgs();
enableQueryFields();
}
}

function manageMsgs() {
clearNonErrorMsg()
Ethkuil marked this conversation as resolved.
Show resolved Hide resolved
if (tableIsEmpty(getTableBody())) {
if (isMsgEmpty()) {
setMsg(UF_MSG_EMPTY_FILTER);
}
hideExportCsvBtn();
} else {
displayCsvExportBtn();
}
}
Expand All @@ -164,41 +176,26 @@ function send(requestPromise, successFn, failureFn) {
() => decrementCounters());
}

/** Fills the first part of a row. */
function build_fork_element_html(table_body, combined_name, num_stars, num_forks) {
const NEW_ROW = $('<tr>', {id: extract_username_from_fork(combined_name), class: "useful_forks_repo"});
table_body.append(
NEW_ROW.append(
$('<td>').html(getRepoCol(combined_name, false)).attr("value", combined_name),
$('<td>').html(UF_TABLE_SEPARATOR + getStarCol(num_stars)).attr("value", num_stars),
$('<td>').html(UF_TABLE_SEPARATOR + getForkCol(num_forks)).attr("value", num_forks)
)
);
return NEW_ROW;
}

/** Add bold to the date text if the date is earlier than the queried repo. */
function compareDates(date, html) {
return REPO_DATE <= new Date(date) ? `<strong>${html}</strong>` : html;
}
Ethkuil marked this conversation as resolved.
Show resolved Hide resolved

/** Prepares, appends, and updates a table row. */
function add_fork_elements(forkdata_array, user, repo, parentDefaultBranch) {
if (isEmpty(forkdata_array))
/** Updates table data, then calls function to update the table. */
function update_table_data(responseData, user, repo, parentDefaultBranch, is_useful_fork) {
if (isEmpty(responseData)) {
return;
}

if (!RATE_LIMIT_EXCEEDED) // because some times gets called after some other msgs are displayed
if (!RATE_LIMIT_EXCEEDED) {// because some times gets called after some other msgs are displayed
payne911 marked this conversation as resolved.
Show resolved Hide resolved
clearNonErrorMsg();
}
payne911 marked this conversation as resolved.
Show resolved Hide resolved

let table_body = getTableBody();
for (const currFork of forkdata_array) {

/* Basic data (name/stars/forks). */
const NEW_ROW = build_fork_element_html(table_body, currFork.full_name, currFork.stargazers_count, currFork.forks_count);

for (const currFork of responseData) {
if (RATE_LIMIT_EXCEEDED) // we can skip everything below because they are only requests
continue;

let datum = {
'name': currFork.full_name,
'stars': currFork.stargazers_count,
'forks': currFork.forks_count,
};

/* Commits diff data (ahead/behind). */
const requestPromise = () => octokit.repos.compareCommits({
owner: user,
Expand All @@ -207,27 +204,22 @@ function add_fork_elements(forkdata_array, user, repo, parentDefaultBranch) {
head: `${extract_username_from_fork(currFork.full_name)}:${currFork.default_branch}`
});
const onSuccess = (responseHeaders, responseData) => {
if (responseData.total_commits <= AHEAD_COMMITS_FILTER) {
NEW_ROW.remove();
if (tableIsEmpty(table_body)) {
setMsg(UF_MSG_EMPTY_FILTER);
if (responseData.total_commits > AHEAD_COMMITS_FILTER) {
datum['ahead_by'] = responseData.ahead_by;
datum['ahead_url'] = responseData.html_url;
datum['behind_by'] = responseData.behind_by;
datum['behind_url'] = getBehindUrl(responseData.html_url);
datum['pushed_at'] = getOnlyDate(currFork.pushed_at);
TABLE_DATA.push(datum);

if (typeof is_useful_fork === 'function') {
Ethkuil marked this conversation as resolved.
Show resolved Hide resolved
update_table(TABLE_DATA.filter(is_useful_fork));
} else {
update_table(TABLE_DATA);
}
} else {
/* Appending the commit badges to the new row. */
const ahead_url = responseData.html_url;
const behind_url = getBehindUrl(ahead_url);
const pushed_at = getOnlyDate(currFork.pushed_at);
const date_txt = compareDates(pushed_at, getDateCol(pushed_at));
NEW_ROW.append(
$('<td>').html(UF_TABLE_SEPARATOR),
$('<td>', {class: "uf_badge"}).html(ahead_badge(responseData.ahead_by, ahead_url)).attr("value", responseData.ahead_by),
$('<td>').html(UF_TABLE_SEPARATOR),
$('<td>', {class: "uf_badge"}).html(behind_badge(responseData.behind_by, behind_url)).attr("value", responseData.behind_by),
$('<td>').html(UF_TABLE_SEPARATOR + date_txt).attr("value", pushed_at)
);
}
};
const onFailure = () => NEW_ROW.remove();
const onFailure = () => { }; // do nothing
send(requestPromise, onSuccess, onFailure);

/* Forks of forks. */
Expand All @@ -237,6 +229,119 @@ function add_fork_elements(forkdata_array, user, repo, parentDefaultBranch) {
}
}

function update_filter() {
let is_useful_fork = getFilterFunction()
if (typeof is_useful_fork === 'function') {
update_table(TABLE_DATA.filter(is_useful_fork));
} else {
update_table(TABLE_DATA);
}
Ethkuil marked this conversation as resolved.
Show resolved Hide resolved

manageMsgs();
}

/**
* Rewrites the table with the specified data.
* @param {Array} data - Array of objects with the following keys: name, stars, forks, ahead_by, ahead_url, behind_by, behind_url, pushed_at
*/
function update_table(data) {
clearTable();
let table_body = getTableBody();
for (const currFork of data) {
const { name, stars, forks, ahead_by, ahead_url, behind_by, behind_url, pushed_at } = currFork;
const date_txt = compareDates(pushed_at, getDateCol(pushed_at));

const NEW_ROW = $('<tr>', { id: extract_username_from_fork(name), class: "useful_forks_repo" });
NEW_ROW.append(
$('<td>').html(getRepoCol(name, false)).attr("value", name),
$('<td>').html(UF_TABLE_SEPARATOR + getStarCol(stars)).attr("value", stars),
$('<td>').html(UF_TABLE_SEPARATOR + getForkCol(forks)).attr("value", forks),
$('<td>').html(UF_TABLE_SEPARATOR),
$('<td>', { class: "uf_badge" }).html(ahead_badge(ahead_by, ahead_url)).attr("value", ahead_by),
$('<td>').html(UF_TABLE_SEPARATOR),
$('<td>', { class: "uf_badge" }).html(behind_badge(behind_by, behind_url)).attr("value", behind_by),
$('<td>').html(UF_TABLE_SEPARATOR + date_txt).attr("value", pushed_at)
);
table_body.append(NEW_ROW);
}
sortTable();
}

/**
* 1. Empty filter means no filter.
* 2. Filter string is a list of conditions separated by spaces.
* 3. If a condition is invalid, it is ignored, and the rest of the conditions are applied.
*/
function getFilterFunction() {
const filter = getFilterOrDefault();
if (filter === '') {
return; // no filter
}

const mapTable = {
'ahead': 'ahead_by',
'behind': 'behind_by',
'pushed': 'pushed_at',
payne911 marked this conversation as resolved.
Show resolved Hide resolved
'a': 'ahead_by',
'b': 'behind_by',
'p': 'pushed_at',
's': 'stars',
'f': 'forks',
};

// parse filter string into condition object
const conditionStrList = filter.split(' ');
let conditionObj = {};
for (const condition of conditionStrList) {
let [attribute, requirement] = condition.split(':');
if (!attribute || !requirement) {
continue; // invalid condition
}
const [_, operator, value] = requirement.split(/([<>=]+)/);
if (!operator || !value) {
continue; // invalid condition
}
if (attribute in mapTable) {
attribute = mapTable[attribute];
}
conditionObj[attribute] = { operator, value };
}
payne911 marked this conversation as resolved.
Show resolved Hide resolved
// construct filter function 'is_useful_fork'
let is_useful_fork = (datum) => {
for (const [attribute, { operator, value }] of Object.entries(conditionObj)) {
switch (operator) {
case '>':
if (datum[attribute] <= value)
return false;
break;
case '>=':
if (datum[attribute] < value)
return false;
break;
case '<':
if (datum[attribute] >= value)
return false;
break;
case '<=':
if (datum[attribute] > value)
return false;
break;
case '=':
if (datum[attribute] != value)
return false;
break;
}
}
return true;
}
return is_useful_fork;
}

/** Add bold to the date text if the date is earlier than the queried repo. */
function compareDates(date, html) {
return REPO_DATE <= new Date(date) ? `<strong>${html}</strong>` : html;
}

/** Paginated (index starts at 1) recursive forks scan. */
function request_fork_page(page_number, user, repo, defaultBranch) {
if (RATE_LIMIT_EXCEEDED)
Expand Down Expand Up @@ -266,8 +371,12 @@ function request_fork_page(page_number, user, repo, defaultBranch) {
}
}

/* Populate the table. */
add_fork_elements(responseData, user, repo, defaultBranch);
let is_useful_fork = getFilterFunction()
if (typeof is_useful_fork === 'function') {
update_table_data(responseData, user, repo, defaultBranch, is_useful_fork);
} else {
update_table_data(responseData, user, repo, defaultBranch);
}
};
const onFailure = () => displayConditionalErrorMsg();
send(requestPromise, onSuccess, onFailure);
Expand Down Expand Up @@ -418,3 +527,8 @@ JQ_REPO_FIELD.keyup(event => {
if (JQ_REPO_FIELD.val()) {
JQ_SEARCH_BTN.click();
}

JQ_FILTER_FIELD.keyup(event => {
// User updated the filter field, so we need to re-filter the table.
update_filter();
});