diff --git a/src/home/fetch-github/fetch-and-display-previews.ts b/src/home/fetch-github/fetch-and-display-previews.ts index 5a4d10fd..e6edbc8b 100644 --- a/src/home/fetch-github/fetch-and-display-previews.ts +++ b/src/home/fetch-github/fetch-and-display-previews.ts @@ -4,6 +4,7 @@ import { applyAvatarsToIssues, renderGitHubIssues } from "../rendering/render-gi import { renderOrgHeaderLabel } from "../rendering/render-org-header"; import { closeModal } from "../rendering/render-preview-modal"; import { filterIssuesBySearch } from "../sorting/filter-issues-by-search"; +import { filterIssuesByAvailability } from "../sorting/filter-issues-by-availability"; import { Sorting } from "../sorting/generate-sorting-buttons"; import { sortIssuesController } from "../sorting/sort-issues-controller"; import { checkCacheIntegrityAndSyncTasks } from "./cache-integrity"; @@ -12,6 +13,12 @@ export type Options = { ordering: "normal" | "reverse"; }; +export let isFilteringAvailableIssues = true; + +export function swapAvailabilityFilter() { + isFilteringAvailableIssues = !isFilteringAvailableIssues; +} + // start at view based on URL export let isProposalOnlyViewer = new URLSearchParams(window.location.search).get("proposal") === "true"; @@ -87,20 +94,16 @@ export async function displayGitHubIssues({ const sortedIssues = sortIssuesController(cachedTasks, sorting, options); let sortedAndFiltered = sortedIssues.filter(getProposalsOnlyFilter(isProposalOnlyViewer)); sortedAndFiltered = filterIssuesByOrganization(sortedAndFiltered); + sortedAndFiltered = isFilteringAvailableIssues ? filterIssuesByAvailability(sortedAndFiltered) : sortedAndFiltered; renderGitHubIssues(sortedAndFiltered, skipAnimation); applyAvatarsToIssues(); } -export async function searchDisplayGitHubIssues({ - searchText, - skipAnimation = false, -}: { - searchText: string; - skipAnimation?: boolean; -}) { +export async function searchDisplayGitHubIssues({ searchText, skipAnimation = false }: { searchText: string; skipAnimation?: boolean }) { const searchResult = filterIssuesBySearch(searchText); let filteredIssues = searchResult.filter(getProposalsOnlyFilter(isProposalOnlyViewer)); filteredIssues = filterIssuesByOrganization(filteredIssues); + filteredIssues = isFilteringAvailableIssues ? filterIssuesByAvailability(filteredIssues) : filteredIssues; renderGitHubIssues(filteredIssues, skipAnimation); applyAvatarsToIssues(); } diff --git a/src/home/sorting/filter-issues-by-availability.ts b/src/home/sorting/filter-issues-by-availability.ts new file mode 100644 index 00000000..050bd3ff --- /dev/null +++ b/src/home/sorting/filter-issues-by-availability.ts @@ -0,0 +1,8 @@ +import { GitHubIssue } from "../github-types"; + +export function filterIssuesByAvailability(issues: GitHubIssue[]) { + return issues.filter((issue) => { + if (issue.assignee) return false; + return true; + }); +} diff --git a/src/home/sorting/sorting-manager.ts b/src/home/sorting/sorting-manager.ts index 58da05fa..cbe7ee09 100644 --- a/src/home/sorting/sorting-manager.ts +++ b/src/home/sorting/sorting-manager.ts @@ -2,11 +2,12 @@ import { displayGitHubIssues, searchDisplayGitHubIssues } from "../fetch-github/ import { renderErrorInModal } from "../rendering/display-popup-modal"; import { proposalViewToggle } from "../rendering/render-github-issues"; import { Sorting } from "./generate-sorting-buttons"; +import { isFilteringAvailableIssues, swapAvailabilityFilter } from "../fetch-github/fetch-and-display-previews"; export class SortingManager { private _lastChecked: HTMLInputElement | null = null; private _toolBarFilters: HTMLElement; - private _filterTextBox: HTMLInputElement; + private _filtersDiv: HTMLElement; private _sortingButtons: HTMLElement; private _instanceId: string; private _sortingState: { [key: string]: "unsorted" | "ascending" | "descending" } = {}; // Track state for each sorting option @@ -20,8 +21,12 @@ export class SortingManager { // Initialize sorting buttons first this._sortingButtons = this._generateSortingButtons(sortingOptions); - // Then initialize filter text box - this._filterTextBox = this._generateFilterTextBox(); + // Initialize filters div + this._filtersDiv = this._generateFiltersDiv(); + // Add filter search box to filters div + this._filtersDiv.appendChild(this._generateFilterTextBox()); + // Add filter available issues button to filters div + this._filtersDiv.appendChild(this._generateFilterAvailableIssuesButton()); // Initialize sorting states to 'unsorted' for all options sortingOptions.forEach((option) => { @@ -30,10 +35,16 @@ export class SortingManager { } public render() { - this._toolBarFilters.appendChild(this._filterTextBox); + this._toolBarFilters.appendChild(this._filtersDiv); this._toolBarFilters.appendChild(this._sortingButtons); } + private _generateFiltersDiv() { + const div = document.createElement("div"); + div.className = "filters"; + return div; + } + private _generateFilterTextBox() { const textBox = document.createElement("input"); textBox.type = "text"; @@ -115,10 +126,19 @@ export class SortingManager { return textBox; } + private _resetSearchBar() { + const filterTextBox = this._filtersDiv.querySelector('input[type="text"]') as HTMLInputElement; + filterTextBox.value = ""; + const newURL = new URL(window.location.href); + newURL.searchParams.delete("search"); + window.history.replaceState({}, "", newURL.toString()); + } + private _resetSortButtons() { this._sortingButtons.querySelectorAll('input[type="radio"]').forEach((input) => { if (input instanceof HTMLInputElement) { input.checked = false; + this._sortingState[input.value] = "unsorted"; input.setAttribute("data-ordering", ""); } }); @@ -148,6 +168,54 @@ export class SortingManager { return buttons; } + private _generateFilterAvailableIssuesButton() { + const input = document.createElement("input"); + input.type = "button"; + input.value = "Unassigned"; + input.id = `filter-availability-${this._instanceId}`; + + input.addEventListener("click", () => { + swapAvailabilityFilter(); + input.value = isFilteringAvailableIssues ? "Unassigned" : "All Issues"; + + try { + // Clear search when applying the filter + this._resetSearchBar(); + + const { sortingOption, sortingOrder } = this._detectSortingState(); + void displayGitHubIssues({ + sorting: sortingOption as Sorting, + options: { ordering: sortingOrder }, + }); + } catch (error) { + renderErrorInModal(error as Error); + } + }); + + return input; + } + + private _detectSortingState() { + let sortingOption; + let sortingOrder = "normal"; + + for (const option of Object.keys(this._sortingState)) { + const order = this._sortingState[option]; + + if (order !== "unsorted") { + sortingOption = option; + + if (order === "descending") { + sortingOrder = "normal"; + } else if (order === "ascending") { + sortingOrder = "reverse"; + } + break; + } + } + return { sortingOption, sortingOrder }; + } + private _createRadioButton(option: string): HTMLInputElement { const input = document.createElement("input"); input.type = "radio"; @@ -168,35 +236,26 @@ export class SortingManager { const currentOrdering = input.getAttribute("data-ordering"); let newOrdering: string; + // Reset sort buttons + this._resetSortButtons(); + // Determine the new ordering based on the current state if (currentOrdering === "normal") { newOrdering = "reverse"; + this._sortingState[option] = "ascending"; } else if (currentOrdering === "reverse") { newOrdering = "disabled"; + this._sortingState[option] = "unsorted"; } else { newOrdering = "normal"; + this._sortingState[option] = "descending"; } // Apply the new ordering state input.setAttribute("data-ordering", newOrdering); - input.parentElement?.childNodes.forEach((node) => { - if (node instanceof HTMLInputElement) { - node.setAttribute("data-ordering", ""); - } - }); // Clear search when applying a different sort - this._filterTextBox.value = ""; - const newURL = new URL(window.location.href); - newURL.searchParams.delete("search"); - window.history.replaceState({}, "", newURL.toString()); - - // Reset other buttons - input.parentElement?.childNodes.forEach((node) => { - if (node instanceof HTMLInputElement) { - node.setAttribute("data-ordering", ""); - } - }); + this._resetSearchBar(); if (newOrdering === "disabled") { this._lastChecked = null; @@ -209,7 +268,10 @@ export class SortingManager { // Apply the sorting based on the new state (normal or reverse) try { - void displayGitHubIssues({ sorting: option as Sorting, options: { ordering: newOrdering } }); + void displayGitHubIssues({ + sorting: option as Sorting, + options: { ordering: newOrdering }, + }); } catch (error) { renderErrorCatch(error as ErrorEvent); } diff --git a/static/style/inverted-style.css b/static/style/inverted-style.css index f1f7836e..7bc2e392 100644 --- a/static/style/inverted-style.css +++ b/static/style/inverted-style.css @@ -1,54 +1,3 @@ -@media (orientation: portrait) or (max-width: 1005px) { - #filters { - display: none; - } - .filters-container label:not([for="view-toggle"]) { - width: 48px; - } - #bottom-right { - display: none; - } - #toolbar { - justify-content: space-between; - } - #bottom-bar { - display: flex; - height: 72px; - width: 100%; - overflow: hidden; - } - .preview-active #bottom-bar { - align-items: center; - } - .labels.cloned-labels { - display: none; - } - body.preview-active .labels.cloned-labels { - flex-grow: 1; - padding: 0px 6px; - display: flex; - width: 0%; - } - #filters-bottom { - flex-direction: column; - gap: 4px; - } - .labels { - margin: 0; - align-items: center; - width: 100%; - } - #filter-bottom { - max-width: 100%; - margin: 0; - } -} -@media (orientation: landscape) and (width > 800px) { - #bottom-bar, - #filters-bottom { - display: none; - } -} @media (prefers-color-scheme: light) { :root { --grid-background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABYWlDQ1BrQ0dDb2xvclNwYWNlRGlzcGxheVAzAAAokWNgYFJJLCjIYWFgYMjNKykKcndSiIiMUmB/yMAOhLwMYgwKicnFBY4BAT5AJQwwGhV8u8bACKIv64LMOiU1tUm1XsDXYqbw1YuvRJsw1aMArpTU4mQg/QeIU5MLikoYGBhTgGzl8pICELsDyBYpAjoKyJ4DYqdD2BtA7CQI+whYTUiQM5B9A8hWSM5IBJrB+API1klCEk9HYkPtBQFul8zigpzESoUAYwKuJQOUpFaUgGjn/ILKosz0jBIFR2AopSp45iXr6SgYGRiaMzCAwhyi+nMgOCwZxc4gxJrvMzDY7v////9uhJjXfgaGjUCdXDsRYhoWDAyC3AwMJ3YWJBYlgoWYgZgpLY2B4dNyBgbeSAYG4QtAPdHFacZGYHlGHicGBtZ7//9/VmNgYJ/MwPB3wv//vxf9//93MVDzHQaGA3kAFSFl7jXH0fsAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAACNJREFUSA3t0IEAAAAMBKFHm7/UTaQQWnXDgAEDBgwYMGDgAXaJAz4RVVHYAAAAAElFTkSuQmCC"); @@ -238,7 +187,7 @@ .labels > img { margin-left: 4px; } - label:not([for="view-toggle"]) { + label:not([for="view-toggle"]), .filters input[type="button"] { line-height: 1.25; padding: 4px 6px; border-radius: 4px; @@ -280,7 +229,7 @@ input[type="radio"]:checked + label:not([for="view-toggle"]) { background-color: #7f7f7f80 !important; } - .labels { + .labels, .filters { display: flex; margin-left: 2px; justify-content: flex-end; @@ -432,7 +381,7 @@ display: none; } } - @media screen and (max-width: 1361px) { + @media screen and (max-width: 1385px) { .partner { margin-top: 0; } @@ -681,7 +630,7 @@ /* width: 100%; */ /* justify-content: normal; */ } - .filters-container input[type="text"] { + .filters input[type="text"] { padding: 4px; border: none; border-radius: 4px; @@ -689,16 +638,29 @@ height: 16px; text-align: center; } + .filters input[type="button"] { + box-sizing: content-box; + border: none; + width: 88px; + cursor: pointer; + } + #filter-availability-top { + margin-right: 4px; + } #filter-top { - width: calc(100vw / 3); + width: calc(80vw / 3); + margin-right: 2px; } - .filters-container input[type="text"]::placeholder { + .filters input[type="text"]::placeholder { font-size: 12px; letter-spacing: 0.5px; } - .filters-container input[type="text"].hidden { + .filters input[type="text"].hidden { display: none; } + .filters input[type="button"]:hover { + background-color: #7f7f7f40; + } button#github-login-button { margin-left: auto; } @@ -956,3 +918,57 @@ border-radius: 4px; } } + +@media (orientation: portrait) or (max-width: 1060px) { + #filters { + display: none; + } + .filters-container label:not([for="view-toggle"]), .filters input[type="button"] { + width: 48px; + } + #bottom-right { + display: none; + } + #toolbar { + justify-content: space-between; + } + #bottom-bar { + display: flex; + height: 72px; + width: 100%; + overflow: hidden; + } + .preview-active #bottom-bar { + align-items: center; + } + .labels.cloned-labels { + display: none; + } + body.preview-active .labels.cloned-labels { + flex-grow: 1; + padding: 0px 6px; + display: flex; + width: 0%; + } + #filters-bottom { + flex-direction: column; + gap: 4px; + } + .labels, .filters { + display: flex; + margin: 0; + align-items: center; + width: 100%; + } + #filter-bottom { + width: 180px; + flex-grow: 12; + margin: 0 2px 0; + } +} +@media (orientation: landscape) and (width > 1060px) { + #bottom-bar, + #filters-bottom { + display: none; + } +} diff --git a/static/style/style.css b/static/style/style.css index 7080a8d0..c33b86a5 100644 --- a/static/style/style.css +++ b/static/style/style.css @@ -1,54 +1,3 @@ -@media (orientation: portrait) or (max-width: 1005px) { - #filters { - display: none; - } - .filters-container label:not([for="view-toggle"]) { - width: 48px; - } - #bottom-right { - display: none; - } - #toolbar { - justify-content: space-between; - } - #bottom-bar { - display: flex; - height: 72px; - width: 100%; - overflow: hidden; - } - .preview-active #bottom-bar { - align-items: center; - } - .labels.cloned-labels { - display: none; - } - body.preview-active .labels.cloned-labels { - flex-grow: 1; - padding: 0px 6px; - display: flex; - width: 0%; - } - #filters-bottom { - flex-direction: column; - gap: 4px; - } - .labels { - margin: 0; - align-items: center; - width: 100%; - } - #filter-bottom { - max-width: 100%; - margin: 0; - } -} -@media (orientation: landscape) and (width > 800px) { - #bottom-bar, - #filters-bottom { - display: none; - } -} @media (prefers-color-scheme: dark) { :root { --grid-background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABYWlDQ1BrQ0dDb2xvclNwYWNlRGlzcGxheVAzAAAokWNgYFJJLCjIYWFgYMjNKykKcndSiIiMUmB/yMAOhLwMYgwKicnFBY4BAT5AJQwwGhV8u8bACKIv64LMOiU1tUm1XsDXYqbw1YuvRJsw1aMArpTU4mQg/QeIU5MLikoYGBhTgGzl8pICELsDyBYpAjoKyJ4DYqdD2BtA7CQI+whYTUiQM5B9A8hWSM5IBJrB+API1klCEk9HYkPtBQFul8zigpzESoUAYwKuJQOUpFaUgGjn/ILKosz0jBIFR2AopSp45iXr6SgYGRiaMzCAwhyi+nMgOCwZxc4gxJrvMzDY7v////9uhJjXfgaGjUCdXDsRYhoWDAyC3AwMJ3YWJBYlgoWYgZgpLY2B4dNyBgbeSAYG4QtAPdHFacZGYHlGHicGBtZ7//9/VmNgYJ/MwPB3wv//vxf9//93MVDzHQaGA3kAFSFl7jXH0fsAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAACNJREFUSA3t0IEAAAAMBKFHm7/UTaQQWnXDgAEDBgwYMGDgAXaJAz4RVVHYAAAAAElFTkSuQmCC"); @@ -238,7 +187,7 @@ .labels > img { margin-left: 4px; } - label:not([for="view-toggle"]) { + label:not([for="view-toggle"]), .filters input[type="button"] { line-height: 1.25; padding: 4px 6px; border-radius: 4px; @@ -280,7 +229,7 @@ input[type="radio"]:checked + label:not([for="view-toggle"]) { background-color: #80808080 !important; } - .labels { + .labels, .filters { display: flex; margin-left: 2px; justify-content: flex-end; @@ -432,7 +381,7 @@ display: none; } } - @media screen and (max-width: 1361px) { + @media screen and (max-width: 1385px) { .partner { margin-top: 0; } @@ -681,7 +630,7 @@ /* width: 100%; */ /* justify-content: normal; */ } - .filters-container input[type="text"] { + .filters input[type="text"] { padding: 4px; border: none; border-radius: 4px; @@ -689,16 +638,29 @@ height: 16px; text-align: center; } + .filters input[type="button"] { + box-sizing: content-box; + border: none; + width: 88px; + cursor: pointer; + } + #filter-availability-top { + margin-right: 4px; + } #filter-top { - width: calc(100vw / 3); + width: calc(80vw / 3); + margin-right: 2px; } - .filters-container input[type="text"]::placeholder { + .filters input[type="text"]::placeholder { font-size: 12px; letter-spacing: 0.5px; } - .filters-container input[type="text"].hidden { + .filters input[type="text"].hidden { display: none; } + .filters input[type="button"]:hover { + background-color: #80808040; + } button#github-login-button { margin-left: auto; } @@ -956,3 +918,57 @@ border-radius: 4px; } } + +@media (orientation: portrait) or (max-width: 1060px) { + #filters { + display: none; + } + .filters-container label:not([for="view-toggle"]), .filters input[type="button"] { + width: 48px; + } + #bottom-right { + display: none; + } + #toolbar { + justify-content: space-between; + } + #bottom-bar { + display: flex; + height: 72px; + width: 100%; + overflow: hidden; + } + .preview-active #bottom-bar { + align-items: center; + } + .labels.cloned-labels { + display: none; + } + body.preview-active .labels.cloned-labels { + flex-grow: 1; + padding: 0px 6px; + display: flex; + width: 0%; + } + #filters-bottom { + flex-direction: column; + gap: 4px; + } + .labels, .filters { + display: flex; + margin: 0; + align-items: center; + width: 100%; + } + #filter-bottom { + width: 180px; + flex-grow: 12; + margin: 0 2px 0; + } +} +@media (orientation: landscape) and (width > 1060px) { + #bottom-bar, + #filters-bottom { + display: none; + } +}