diff --git a/themes/vocabulary_theme/templates/issue_finder.html b/themes/vocabulary_theme/templates/issue_finder.html index fdf625d2b..595f4182b 100644 --- a/themes/vocabulary_theme/templates/issue_finder.html +++ b/themes/vocabulary_theme/templates/issue_finder.html @@ -3,40 +3,58 @@ {% block title %}{{ this.title }}{% endblock %} {% block body %} -
-
-
-

{{ this.title }}

-
- {{ this.description }} + +
+

{{ this.title }}

+

{{ this.description }}

+
+
+
+
+
+ +
+
+ + +
+
+ + +
+ + +
+

+ *Not all issues have skills marked on them, especially if they are simple issues that do not require proficiency in any specific framework or language. Those issues will not appear when filtering by skill. +

+
+
+ + +
+

Loading issues...

+
-
-
- {{ this.body }} - -
-
{% endblock %} diff --git a/webpack/js/components.js b/webpack/js/components.js deleted file mode 100644 index 3b7fc3b2e..000000000 --- a/webpack/js/components.js +++ /dev/null @@ -1,265 +0,0 @@ -import VueSelect from 'vue-select'; -import {Octokit} from '@octokit/rest'; -import yaml from 'js-yaml' - -import {hydrateAppWithData} from "./hydration"; - -export const IssueLabel = { - template: ` - - {{ name }} -`, - data() { - return { - labels: window.labels - } - }, - props: { - name: { - type: String, - required: true - } - }, - computed: { - /** - * Get the name of the class to apply to the label based on the group to - * which it belongs. Falls back to miscellaneous if the label does not - * belong to a group or a class cannot be identified. - * - * @returns {string} the name of the class to apply to the label - */ - className() { - return this.$root.categories[this.name] || `${this.name.toLocaleLowerCase()} miscellaneous` - } - } -} - -export const IssueCard = { - template: ` -
-

- {{ issue.title }} -

-

- - - {{ issue.repo }}#{{ issue.number }} - - - - -   opened on {{ dateCreated }}. -

-
- -
-
`, - components: { - IssueLabel - }, - props: { - issue: { - type: Object, - required: true - }, - }, - computed: { - dateCreated() { - const [dateComponent,] = this.issue.created_at.split("T") - return dateComponent - } - } -} - -export const App = { - el: '#vue-app', - template: ` -
-
-
-
- - -
- - -
- Loading filters, please wait... -
-
-
- -

- No results. -

-
-
- -
`, - components: { - VueSelect, - IssueCard - }, - data() { - return { - options: { - aims: [ - {name: 'Contributing code', code: 'contribute'}, - {name: 'Triaging issues', code: 'triage'}, - {name: 'Labelling issues', code: 'label'} - ], - skills: [], - experiences: [ - {name: 'Yes, it is', code: 'beginner'}, - {name: 'No, it isn\'t', code: 'experienced'} - ] - }, - filters: { - aim: 'contribute', - skills: [], - experience: 'experienced' - }, - categories: {}, - issues: [], - octokit: null - } - }, - computed: { - /** - * Get a filtered list of issues matching the chosen skill labels. - * - * @returns {array} the array of filtered issues - */ - filteredIssues() { - return this.issues.filter(issue => { - // If aim is to triage issues - if (this.filters.aim === 'triage' || this.filters.aim === 'label') { - // Show all issues as they all have the label "🚦 status: awaiting triage" - return true - } - - // Check experience match - if (this.filters.experience === 'beginner' && !issue.labels.includes('good first issue')) { - return false - } - - // Check skill set match - const joinedLabels = issue.labels.join(',') - return !(this.filters.skills.length && !this.filters.skills.some(skill => joinedLabels.includes(skill))); - }).sort((a, b) => b.createdAt - a.createdAt) - } - }, - watch: { - 'filters.aim' (to, from) { - if (to !== from) { - this.loadIssues() - } - } - }, - methods: { - loadIssues () { - const q = ['org:creativecommons', 'is:open', 'is:issue'] - if (this.filters.aim === 'contribute') { - q.push('label:"help wanted"') - } else if (this.filters.aim === 'triage') { - q.push('label:"🚦 status: awaiting triage"') - } else if (this.filters.aim === 'label') { - q.push('label:"🏷 status: label work required"') - } - this.octokit.search.issuesAndPullRequests({ - q: q.join(' '), - per_page: 100, - sort: 'created', - order: 'desc' - }).then(res => { - this.issues = res.data.items - this.issues.forEach(issue => { - issue.labels = issue.labels.map(label => label.name) - - const repoUrl = issue.repository_url - issue.repo = repoUrl.slice(repoUrl.lastIndexOf('/') + 1) - }) - }) - } - }, - mounted() { - const BASE_URL = 'https://raw.githubusercontent.com/creativecommons/ccos-scripts/main/ccos/norm' - const FILE_URL = name => `${BASE_URL}/${name}.yml` - - this.octokit = new Octokit() - this.loadIssues() - - Promise - .all([ - fetch(FILE_URL('skills')) - .then(response => response.text()), - fetch(FILE_URL('labels')) - .then(response => response.text()) - ]) - .then(([skillResponse, labelResponse]) => { - skillResponse = yaml.safeLoad(skillResponse) - labelResponse = yaml.safeLoad(labelResponse) - const [skills, categories] = hydrateAppWithData(skillResponse, labelResponse) - this.categories = categories - this.options.skills = skills - }) - .catch(err => console.error(err)) - } -} diff --git a/webpack/js/hydration.js b/webpack/js/hydration.js deleted file mode 100644 index a42da9f5d..000000000 --- a/webpack/js/hydration.js +++ /dev/null @@ -1,42 +0,0 @@ -export const hydrateAppWithData = (skills, labels) => { - skills = Array.from( - new Set( // Remove duplicates - Object.values(skills).flat() // Combine all skills - ) - ) - const top_level_skills = Array.from( - new Set( // Remove duplicates - skills.map(skill => skill.split('/')[0]) // Keep only the prefix - ) - ) - - const categories = {} - labels.groups.forEach(group => { - group.labels.forEach(label => { - let name = label.name - if (group.is_prefixed !== false) { - name = `${group.name}: ${name}` - } - if (label.has_emoji_name !== false) { - name = `${label.emoji} ${name}` - } - let styleName = label.color; - if (/^[A-Z]+$/.test(styleName)) { - styleName = `${group.name}-${styleName.toLocaleLowerCase()}` - } else { - styleName = group.name - } - categories[name] = styleName - }) - }) - labels.standalone.forEach(label => { - let name = `${label.emoji} ${label.name}` - categories[name] = 'miscellaneous' - }) - skills.forEach(skill => { - let name = `πŸ’ͺ skill: ${skill.toLocaleLowerCase()}` - categories[name] = 'skill' - }) - - return [top_level_skills, categories] -} diff --git a/webpack/js/issue-finder.js b/webpack/js/issue-finder.js index 5a52f7752..595bbb5d2 100644 --- a/webpack/js/issue-finder.js +++ b/webpack/js/issue-finder.js @@ -1,7 +1,180 @@ -import Vue from 'vue'; -import {App} from './components' +import { Octokit } from "@octokit/rest"; // GitHub API +import yaml from "js-yaml"; // Parse YAML files into JS objects -$(document).ready(function () { - if (window.location.pathname == '/contributing-code/issue-finder/') - window.app = new Vue(App) -}) +// Wait for the DOM to fully load before running the script +document.addEventListener("DOMContentLoaded", function () { + // HTML element references + const issuesContainer = document.getElementById("issues-container"); + const aimDropdown = document.getElementById("aim"); + const skillsDropdown = document.getElementById("skills"); + const experienceDropdown = document.getElementById("experience"); + const skillFiltersDiv = document.getElementById("skill-filters"); + + // Base URL for fetching YAML data + const BASE_YAML_URL = + "https://raw.githubusercontent.com/creativecommons/ccos-scripts/main/ccos/norm"; + const getYamlFileUrl = (fileName) => `${BASE_YAML_URL}/${fileName}.yml`; + + // Initial state for issues and filters + let issuesList = []; // + const userFilters = { + aim: "contribute", // Default to "Contribute" + skills: [], // Default to "No preferences" + experience: "experienced", // Default to "Experienced" + }; + + // Fetch and load filter data (skills and labels) + async function fetchFilterData() { + try { + const [skillsYaml, labelsYaml] = await Promise.all([ + fetch(getYamlFileUrl("skills")).then((res) => res.text()), + fetch(getYamlFileUrl("labels")).then((res) => res.text()), + ]); + + const skills = yaml.safeLoad(skillsYaml); + populateSkillsDropdown(Object.values(skills).flat()); // Populate skills dropdown + } catch (error) { + console.error("Error loading filters:", error); + } + } + +function populateSkillsDropdown(skills) { + // Process skills to remove duplicates + const uniqueSkills = Array.from(new Set(Object.values(skills).flat())); // Flatten and remove duplicates + const topLevelSkills = Array.from( + new Set(uniqueSkills.map((skill) => skill.split("/")[0])) + ); // Extract prefixes + + // Populate skills dropdown + const skillsDropdown = document.getElementById("skills"); + skillsDropdown.innerHTML = ""; // Clear existing options + const noPreferenceOption = document.createElement("option"); + noPreferenceOption.value = ""; // Empty value for no preference + noPreferenceOption.textContent = "No preferences"; + skillsDropdown.appendChild(noPreferenceOption); + // Add each top-level skill to the dropdown + topLevelSkills.forEach((skill) => { + const optionElement = document.createElement("option"); + optionElement.value = skill.toLowerCase(); + optionElement.textContent = skill; + skillsDropdown.appendChild(optionElement); + }); +} + + // Build GitHub search query based on user-selected filters + function loadIssues() { + const query = ["org:creativecommons", "is:open", "is:issue"]; + + if (userFilters.aim === "contribute") query.push('label:"help wanted"'); + if (userFilters.aim === "triage") + query.push('label:"🚦 status: awaiting triage"'); + if (userFilters.aim === "label") + query.push('label:"🏷 status: label work required"'); + + return query.join(" "); + } + + // Fetch and display GitHub issues based on filters + async function fetchAndDisplayIssues() { + try { + const octokit = new Octokit(); + const query = loadIssues(); + const response = await octokit.search.issuesAndPullRequests({ + q: query, + per_page: 100, + sort: "created", + order: "desc", + }); + + // Extract needed data from issues + issuesList = response.data.items.map((issue) => ({ + ...issue, + labels: issue.labels.map((label) => label.name), + repo: issue.repository_url.split("/").pop(), + })); + + displayFilteredIssues(); // Display filtered issues + } catch (error) { + console.error("Error loading GitHub issues:", error); + } + } + + // function to display filtered issues + function displayFilteredIssues() { + issuesContainer.innerHTML = ""; // Clear any existing issues + + const filteredIssues = issuesList.filter((issue) => { + if (userFilters.aim !== "contribute") return true; + + // Show only "good first issues" for beginners + if ( + userFilters.experience === "beginner" && + !issue.labels.includes("good first issue") + ) + return false; + + // Check if issue matches selected skills + if ( + userFilters.skills.length && + !userFilters.skills.some((skill) => + issue.labels.includes(`πŸ’ͺ skill: ${skill}`) + ) + ) + return false; + + return true; // Show issue if it meets all criteria + }); + + // Show "No results" message if no issues match + if (filteredIssues.length === 0) { + issuesContainer.innerHTML = "

No results.

"; + return; + } + + // Create an issue card for each filtered issue + filteredIssues.forEach((issue) => { + const issueCard = document.createElement("div"); + issueCard.className = "issue-card"; + issueCard.innerHTML = ` +

${issue.title}

+

+ + ${issue.repo}#${issue.number} + + opened on ${issue.created_at.split("T")[0]}. +

+
+ ${issue.labels + .map((label) => `${label}`) + .join("")} +
+ `; + issuesContainer.appendChild(issueCard); + }); + } + + // Update filters and reload issues when the user changes selections + aimDropdown.addEventListener("change", (event) => { + userFilters.aim = event.target.value; + skillFiltersDiv.style.display = + userFilters.aim === "contribute" ? "block" : "none"; + fetchAndDisplayIssues(); + }); + + skillsDropdown.addEventListener("change", (event) => { + const selectedSkills = Array.from(event.target.selectedOptions).map( + (option) => option.value + ); + userFilters.skills = selectedSkills.includes("") ? [] : selectedSkills; + displayFilteredIssues(); + }); + + experienceDropdown.addEventListener("change", (event) => { + userFilters.experience = event.target.value; + displayFilteredIssues(); + }); + + // Load filters and issues when the page loads + fetchFilterData(); + fetchAndDisplayIssues(); +});