Repository navigation
Sorting options #331
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
Open
maryLLay
wants to merge
16
commits into
JaneliaSciComp:main
Choose a base branch
from
maryLLay:sorting_options
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Sorting options #331
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
e63cf40
feat: sort projects alphabetically
maryLLay bfd40f0
feat: add option to sort by last update
maryLLay 651df17
Chore: adjust filter menu font
maryLLay 46ca16a
Merge remote-tracking branch 'origin/main' into sorting_options
allison-truhlar 92181c3
feat: move sort options into their own SortMenu component
allison-truhlar ced0d05
fix: align filter menu spacing and show its title on all screens
allison-truhlar c10f066
Merge remote-tracking branch 'upstream/main' into sorting_options
maryLLay 6203ab0
Chore: update package-lock
maryLLay 889e549
perf: pass per-card sortOrder instead of full sortedTitlesMap to Card…
maryLLay 0406991
chore: remove unused repo-dates.json from update-project-dates workflow
maryLLay 6e320b9
fix: tolerate partial GraphQL results in update-project-dates
maryLLay de4c3c9
fix: trigger deploy after bot commits updated project dates
maryLLay 754205c
Merge remote-tracking branch 'upstream/main' into sorting_options
maryLLay d26162a
fix: add update date to project md
maryLLay 2fa97bd
fix: correct update_date field in projects content schema
maryLLay 8c4189c
fix: use grid layout at all breakpoints so card sort order applies on…
maryLLay File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| import { readFileSync, writeFileSync, readdirSync } from "fs"; | ||
| import { fileURLToPath } from "url"; | ||
| import { dirname, join } from "path"; | ||
| import yaml from "yaml"; | ||
| import { graphql } from "@octokit/graphql"; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const PROJECTS_DIR = join(__dirname, "..", "..", "src", "content", "projects"); | ||
|
|
||
| const authToken = process.env.GITHUB_TOKEN || process.env.OSSI_SITE_TOKEN; | ||
| if (!authToken) { | ||
| console.error( | ||
| "Missing GITHUB_TOKEN (or OSSI_SITE_TOKEN) environment variable." | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const graphqlWithAuth = graphql.defaults({ | ||
| headers: { authorization: `token ${authToken}` }, | ||
| }); | ||
|
|
||
| // extracts the frontmatter block, split into the pieces needed to splice a | ||
| // single field back in without reserializing (and reformatting) the rest of it | ||
| function extractFrontMatter(content) { | ||
| const match = content.match(/^(---\n)([\s\S]+?)(\n---)/); | ||
| return match | ||
| ? { full: match[0], prefix: match[1], body: match[2], suffix: match[3] } | ||
| : null; | ||
| } | ||
|
|
||
| function parseOwnerRepo(githubUrl) { | ||
| let url; | ||
| try { | ||
| url = new URL(githubUrl); | ||
| } catch { | ||
| return null; | ||
| } | ||
| const parts = url.pathname.split("/").filter((part) => part !== ""); | ||
| if (parts.length < 2) return null; | ||
| return { owner: parts[0], repo: parts[1].replace(/\.git$/, "") }; | ||
| } | ||
|
|
||
| function upsertField(frontMatterBody, key, value) { | ||
| const lineRegex = new RegExp(`^${key}:.*$`, "m"); | ||
| const newLine = `${key}: ${value}`; | ||
| if (lineRegex.test(frontMatterBody)) { | ||
| return frontMatterBody.replace(lineRegex, newLine); | ||
| } | ||
| const githubLinkRegex = /^(github link:.*)$/m; | ||
| if (githubLinkRegex.test(frontMatterBody)) { | ||
| return frontMatterBody.replace(githubLinkRegex, `$1\n${newLine}`); | ||
| } | ||
| return `${frontMatterBody}\n${newLine}`; | ||
| } | ||
|
|
||
| // 1. gather project files and the owner/repo their "github link" points to | ||
| const projectFiles = readdirSync(PROJECTS_DIR).filter( | ||
| (file) => file.endsWith(".md") && !file.startsWith("_") | ||
| ); | ||
|
|
||
| const projectRepos = []; | ||
| for (const file of projectFiles) { | ||
| const filePath = join(PROJECTS_DIR, file); | ||
| const content = readFileSync(filePath, "utf8"); | ||
| const frontMatter = extractFrontMatter(content); | ||
| if (!frontMatter) { | ||
| console.warn(`Skipping ${file}: no frontmatter found.`); | ||
| continue; | ||
| } | ||
|
|
||
| const parsed = yaml.parse(frontMatter.body); | ||
| const githubUrl = parsed["github link"]; | ||
| const ownerRepo = githubUrl ? parseOwnerRepo(githubUrl) : null; | ||
| if (!ownerRepo) { | ||
| console.warn( | ||
| `Skipping ${file}: could not parse "github link" (${githubUrl}).` | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| projectRepos.push({ file, filePath, content, frontMatter, ...ownerRepo }); | ||
| } | ||
|
|
||
| // 2. batch-fetch pushedAt for every unique repo in a single GraphQL call | ||
| const repoKey = (owner, repo) => `${owner}/${repo}`.toLowerCase(); | ||
| const uniqueRepos = [ | ||
| ...new Map( | ||
| projectRepos.map((p) => [repoKey(p.owner, p.repo), p]) | ||
| ).values(), | ||
| ]; | ||
|
|
||
| const queryParts = []; | ||
| const variableDefs = []; | ||
| const variables = {}; | ||
| uniqueRepos.forEach((p, i) => { | ||
| queryParts.push( | ||
| `r${i}: repository(owner: $owner${i}, name: $name${i}) { nameWithOwner pushedAt }` | ||
| ); | ||
| variableDefs.push(`$owner${i}: String!`, `$name${i}: String!`); | ||
| variables[`owner${i}`] = p.owner; | ||
| variables[`name${i}`] = p.repo; | ||
| }); | ||
|
|
||
| const query = `query(${variableDefs.join(", ")}) {\n${queryParts.join( | ||
| "\n" | ||
| )}\n}`; | ||
|
|
||
| let result; | ||
| try { | ||
| result = await graphqlWithAuth(query, variables); | ||
| } catch (error) { | ||
| if (error.data){ | ||
| console.warn("GraphQL returned partial data:", error.message); | ||
| result = error.data; | ||
| } | ||
| else { | ||
| console.error("GraphQL query failed:", error.message); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| // 3. build repoDates in memory: repo (owner/name, lowercased) -> pushedAt | ||
| const repoDates = {}; | ||
| uniqueRepos.forEach((p, i) => { | ||
| const repoResult = result[`r${i}`]; | ||
| if (repoResult && repoResult.pushedAt) { | ||
| repoDates[repoKey(p.owner, p.repo)] = repoResult.pushedAt; | ||
| } else { | ||
| console.warn( | ||
| `No pushedAt found for ${p.owner}/${p.repo} (referenced by ${p.file})` | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| // 4. splice the resulting update_date into each project's frontmatter | ||
| let updatedCount = 0; | ||
| for (const project of projectRepos) { | ||
| const pushedAt = repoDates[repoKey(project.owner, project.repo)]; | ||
| if (!pushedAt) continue; | ||
|
|
||
| const updateDate = pushedAt.slice(0, 10); // YYYY-MM-DD | ||
| const newBody = upsertField( | ||
| project.frontMatter.body, | ||
| "update_date", | ||
| updateDate | ||
| ); | ||
| if (newBody === project.frontMatter.body) continue; | ||
|
|
||
| const newContent = project.content.replace( | ||
| project.frontMatter.full, | ||
| `${project.frontMatter.prefix}${newBody}${project.frontMatter.suffix}` | ||
| ); | ||
| writeFileSync(project.filePath, newContent); | ||
| updatedCount++; | ||
| } | ||
|
|
||
| console.log(`Updated update_date in ${updatedCount} project file(s).`); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| name: Update project last-updated dates | ||
|
|
||
| # Refreshes the update_date frontmatter field on every project .md file from | ||
| # each project's GitHub repository pushedAt timestamp, so the site's | ||
| # "Last updated" sort stays current without manual upkeep. | ||
| on: | ||
| schedule: | ||
| - cron: "0 6 * * 1" # every Monday at 06:00 UTC | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: write | ||
| actions: write | ||
|
|
||
| jobs: | ||
| update-dates: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| cache: "npm" | ||
| cache-dependency-path: .github/actions/package-lock.json | ||
|
|
||
| - name: Install npm dependencies | ||
| run: npm ci | ||
| working-directory: .github/actions | ||
|
|
||
| - name: Fetch and update project dates | ||
| run: node update-project-dates.js | ||
| working-directory: .github/actions | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Commit and push changes | ||
| id: commit | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git add src/content/projects | ||
| if git diff --cached --quiet; then | ||
| echo "No changes to commit." | ||
| else | ||
| git commit -m "chore: update project last-updated dates" | ||
| git push | ||
|
maryLLay marked this conversation as resolved.
|
||
| echo "pushed=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Trigger deploy | ||
| if: steps.commit.outputs.pushed == 'true' | ||
| run: gh workflow run deploy-to-github-pages.yml | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.